diff --git a/node_modules/@redcode/medium-sdk/.github/workflows/npm-publish.yml b/node_modules/@redcode/medium-sdk/.github/workflows/npm-publish.yml new file mode 100644 index 0000000..f0ee36b --- /dev/null +++ b/node_modules/@redcode/medium-sdk/.github/workflows/npm-publish.yml @@ -0,0 +1,34 @@ +# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created +# For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages + +name: Node.js Package + +on: + release: + types: [created] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: 14 + - run: npm ci + - run: npm test + + publish-npm: + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: 14 + registry-url: https://registry.npmjs.org/ + - run: npm ci + - run: npm publish --access public + env: + NODE_AUTH_TOKEN: ${{secrets.npm_token}} + diff --git a/node_modules/@redcode/medium-sdk/LICENSE b/node_modules/@redcode/medium-sdk/LICENSE new file mode 100644 index 0000000..8f71f43 --- /dev/null +++ b/node_modules/@redcode/medium-sdk/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/node_modules/@redcode/medium-sdk/README.md b/node_modules/@redcode/medium-sdk/README.md new file mode 100644 index 0000000..a304442 --- /dev/null +++ b/node_modules/@redcode/medium-sdk/README.md @@ -0,0 +1,67 @@ +**Warning:** This sdk is no longer supported or maintained by Medium. + + +# Medium SDK for NodeJS + +This repository contains the open source SDK for integrating [Medium](https://medium.com)'s OAuth2 API into your NodeJs app. + +View the full [documentation here](https://github.com/Medium/medium-api-docs). + +Install +------- + + npm install medium-sdk + +Usage +----- + +Create a client, then call commands on it. + +```javascript +var medium = require('medium-sdk') + +var client = new medium.MediumClient({ + clientId: 'YOUR_CLIENT_ID', + clientSecret: 'YOUR_CLIENT_SECRET' +}) + +var redirectURL = 'https://yoursite.com/callback/medium'; + +var url = client.getAuthorizationUrl('secretState', redirectURL, [ + medium.Scope.BASIC_PROFILE, medium.Scope.PUBLISH_POST +]) + +// (Send the user to the authorization URL to obtain an authorization code.) + +client.exchangeAuthorizationCode('YOUR_AUTHORIZATION_CODE', redirectURL, function (err, token) { + client.getUser(function (err, user) { + client.createPost({ + userId: user.id, + title: 'A new post', + contentFormat: medium.PostContentFormat.HTML, + content: '

A New Post

This is my new post.

', + publishStatus: medium.PostPublishStatus.DRAFT + }, function (err, post) { + console.log(token, user, post) + }) + }) +}) +``` + +Contributing +------------ + +Questions, comments, bug reports, and pull requests are all welcomed. If you haven't contributed to a Medium project before please head over to the [Open Source Project](https://github.com/Medium/opensource#note-to-external-contributors) and fill out an OCLA (it should be pretty painless). + +Authors +------- + +[Jamie Talbot](https://github.com/majelbstoat) + +License +------- + +Copyright 2015 [A Medium Corporation](https://medium.com) + +Licensed under Apache License Version 2.0. Details in the attached LICENSE +file. diff --git a/node_modules/@redcode/medium-sdk/index.js b/node_modules/@redcode/medium-sdk/index.js new file mode 100644 index 0000000..d7fd1f3 --- /dev/null +++ b/node_modules/@redcode/medium-sdk/index.js @@ -0,0 +1 @@ +module.exports = require("./lib/mediumClient.js") diff --git a/node_modules/@redcode/medium-sdk/lib/mediumClient.js b/node_modules/@redcode/medium-sdk/lib/mediumClient.js new file mode 100644 index 0000000..e469f81 --- /dev/null +++ b/node_modules/@redcode/medium-sdk/lib/mediumClient.js @@ -0,0 +1,413 @@ +// Copright 2015 A Medium Corporation + +var https = require('https') +var qs = require('querystring') +var url = require('url') +var util = require('util') + + +var DEFAULT_ERROR_CODE = -1 +var DEFAULT_TIMEOUT_MS = 5000 + + +/** + * Valid scope options. + * @enum {string} + */ +var Scope = { + BASIC_PROFILE: 'basicProfile', + LIST_PUBLICATIONS: 'listPublications', + PUBLISH_POST: 'publishPost' +} + + +/** + * The publish status when creating a post. + * @enum {string} + */ +var PostPublishStatus = { + DRAFT: 'draft', + UNLISTED: 'unlisted', + PUBLIC: 'public' +} + + +/** + * The content format to use when creating a post. + * @enum {string} + */ +var PostContentFormat = { + HTML: 'html', + MARKDOWN: 'markdown' +} + + +/** + * The license to use when creating a post. + * @enum {string} + */ +var PostLicense = { + ALL_RIGHTS_RESERVED: 'all-rights-reserved', + CC_40_BY: 'cc-40-by', + CC_40_BY_ND: 'cc-40-by-nd', + CC_40_BY_SA: 'cc-40-by-sa', + CC_40_BY_NC: 'cc-40-by-nc', + CC_40_BY_NC_ND: 'cc-40-by-nc-nd', + CC_40_BY_NC_SA: 'cc-40-by-nc-sa', + CC_40_ZERO: 'cc-40-zero', + PUBLIC_DOMAIN: 'public-domain' +} + + +/** + * An error with a code. + * + * @param {string} message + * @param {number} code + * @constructor + */ +function MediumError(message, code) { + this.message = message + this.code = code +} +util.inherits(MediumError, Error) + + +/** + * The core client. + * + * @param {{ + * clientId: string, + * clientSecret: string + * }} options + * @constructor + */ +function MediumClient(options = {}) { + this._clientId = options.clientId + this._clientSecret = options.clientSecret + this._accessToken = "" +} + + +/** + * Sets an access token on the client used for making requests. + * + * @param {string} accessToken + * @return {MediumClient} + */ +MediumClient.prototype.setAccessToken = function (accessToken) { + this._accessToken = accessToken + return this +} + + +/** + * Builds a URL at which you may request authorization from the user. + * + * @param {string} state + * @param {string} redirectUrl + * @param {Array.} requestedScope + * @return {string} + */ +MediumClient.prototype.getAuthorizationUrl = function (state, redirectUrl, requestedScope) { + return url.format({ + protocol: 'https', + host: 'medium.com', + pathname: '/m/oauth/authorize', + query: { + client_id: this._clientId, + scope: requestedScope.join(','), + response_type: 'code', + state: state, + redirect_uri: redirectUrl + } + }) +} + + +/** + * Exchanges an authorization code for an access token and a refresh token. + * + * @param {string} code + * @param {string} redirectUrl + * @param {NodeCallback} callback + */ +MediumClient.prototype.exchangeAuthorizationCode = function (code, redirectUrl, callback) { + this._acquireAccessToken({ + code: code, + client_id: this._clientId, + client_secret: this._clientSecret, + grant_type: 'authorization_code', + redirect_uri: redirectUrl + }, callback) +} + + +/** + * Exchanges a refresh token for an access token and a refresh token. + * + * @param {string} refreshToken + * @param {NodeCallback} callback + */ +MediumClient.prototype.exchangeRefreshToken = function (refreshToken, callback) { + this._acquireAccessToken({ + refresh_token: refreshToken, + client_id: this._clientId, + client_secret: this._clientSecret, + grant_type: 'refresh_token' + }, callback) +} + + +/** + * Returns the details of the user associated with the current + * access token. + * + * Requires the current access token to have the basicProfile scope. + * + * @param {NodeCallback} callback + */ +MediumClient.prototype.getUser = function (callback) { + this._makeRequest({ + method: 'GET', + path: '/v1/me' + }, callback) +} + + +/** + * Returns the publications related to the current user. Notice that + * the userId needs to be passed in as an option. It can be acquired + * with a call to getUser(). + * + * Requires the current access token to have the + * listPublications scope. + * + * @param {{ + * userId: string + * }} options + * @param {NodeCallback} callback + */ +MediumClient.prototype.getPublicationsForUser = function (options, callback) { + this._enforce(options, ['userId']) + this._makeRequest({ + method: 'GET', + path: '/v1/users/' + options.userId + '/publications' + }, callback) +} + + +/** + * Returns the contributors for a chosen publication. The publication is identified + * by the publication ID included in the options argument. IDs for publications + * can be acquired by getUsersPublications. + * + * Requires the current access token to have the basicProfile scope. + * + * @param {{ + * publicationId: string + * }} options + * @param {NodeCallback} callback + */ +MediumClient.prototype.getContributorsForPublication = function (options, callback) { + this._enforce(options, ['publicationId']) + this._makeRequest({ + method: 'GET', + path: '/v1/publications/' + options.publicationId + '/contributors' + }, callback) +} + + +/** + * Creates a post on Medium. + * + * Requires the current access token to have the publishPost scope. + * + * @param {{ + * userId: string, + * title: string, + * contentFormat: PostContentFormat, + * content: string, + * tags: Array., + * canonicalUrl: string, + * publishStatus: PostPublishStatus, + * license: PostLicense + * }} options + * @param {NodeCallback} callback + */ +MediumClient.prototype.createPost = function (options, callback) { + this._enforce(options, ['userId']) + this._makeRequest({ + method: 'POST', + path: '/v1/users/' + options.userId + '/posts', + data: { + title: options.title, + content: options.content, + contentFormat: options.contentFormat, + tags: options.tags, + canonicalUrl: options.canonicalUrl, + publishedAt: options.publishedAt, + publishStatus: options.publishStatus, + license: options.license + } + }, callback) +} + + +/** + * Creates a post on Medium and places it under specified publication. + * Please refer to the API documentation for rules around publishing in + * a publication: https://github.com/Medium/medium-api-docs + * + * Requires the current access token to have the publishPost scope. + * + * @param {{ + * userId: string, + * publicationId: string, + * title: string, + * contentFormat: PostContentFormat, + * content: string, + * tags: Array., + * canonicalUrl: string, + * publishStatus: PostPublishStatus, + * license: PostLicense + * }} options + * @param {NodeCallback} callback + */ +MediumClient.prototype.createPostInPublication = function (options, callback) { + this._enforce(options, ['publicationId']) + this._makeRequest({ + method: 'POST', + path: '/v1/publications/' + options.publicationId + '/posts', + data: { + title: options.title, + content: options.content, + contentFormat: options.contentFormat, + tags: options.tags, + canonicalUrl: options.canonicalUrl, + publishedAt: options.publishedAt, + publishStatus: options.publishStatus, + license: options.license + } + }, callback) +} + + +/** + * Acquires an access token for the Medium API. + * + * Sets the access token on the client on success. + * + * @param {Object} params + * @param {NodeCallback} callback + */ +MediumClient.prototype._acquireAccessToken = function (params, callback) { + this._makeRequest({ + method: 'POST', + path: '/v1/tokens', + contentType: 'application/x-www-form-urlencoded', + data: qs.stringify(params) + }, function (err, data) { + if (!err) { + this._accessToken = data.access_token + } + callback(err, data) + }.bind(this)) +} + + +/** + * Enforces that given options object (first param) defines + * all keys requested (second param). Raises an error if any + * is missing. + * + * @param {Object} options + * @param {keys} requiredKeys + */ +MediumClient.prototype._enforce = function (options, requiredKeys) { + if (!options) { + throw new MediumError('Parameters for this call are undefined', DEFAULT_ERROR_CODE) + } + requiredKeys.forEach(function (requiredKey) { + if (!options[requiredKey]) throw new MediumError('Missing required parameter "' + requiredKey + '"', DEFAULT_ERROR_CODE) + }) +} + + + +/** + * Makes a request to the Medium API. + * + * @param {Object} options + * @param {NodeCallback} callback + */ +MediumClient.prototype._makeRequest = function (options, callback) { + var requestParams = { + host: 'api.medium.com', + port: 443, + method: options.method, + path: options.path + } + var req = https.request(requestParams, function (res) { + var body = [] + + res.setEncoding('utf-8') + res.on('data', function (data) { + body.push(data) + }) + res.on('end', function () { + var payload + var responseText = body.join('') + try { + payload = JSON.parse(responseText) + } catch (err) { + callback(new MediumError('Failed to parse response', DEFAULT_ERROR_CODE), null) + return + } + + var statusCode = res.statusCode + var statusType = Math.floor(res.statusCode / 100) + + if (statusType == 4 || statusType == 5) { + var err = payload.errors[0] + callback(new MediumError(err.message, err.code), null) + } else if (statusType == 2) { + callback(null, payload.data || payload) + } else { + callback(new MediumError('Unexpected response', DEFAULT_ERROR_CODE), null) + } + }) + }).on('error', function (err) { + callback(new MediumError(err.message, DEFAULT_ERROR_CODE), null) + }) + + req.setHeader('Content-Type', options.contentType || 'application/json') + req.setHeader('Authorization', 'Bearer ' + this._accessToken) + req.setHeader('Accept', 'application/json') + req.setHeader('Accept-Charset', 'utf-8') + + req.setTimeout(DEFAULT_TIMEOUT_MS, function () { + // Aborting a request triggers the 'error' event. + req.abort() + }) + + if (options.data) { + var data = options.data + if (typeof data == 'object') { + data = JSON.stringify(data) + } + req.write(data) + } + req.end() +} + +// Exports + +module.exports = { + MediumClient: MediumClient, + MediumError: MediumError, + Scope: Scope, + PostPublishStatus: PostPublishStatus, + PostLicense: PostLicense, + PostContentFormat: PostContentFormat +} diff --git a/node_modules/@redcode/medium-sdk/package.json b/node_modules/@redcode/medium-sdk/package.json new file mode 100644 index 0000000..b0e94f9 --- /dev/null +++ b/node_modules/@redcode/medium-sdk/package.json @@ -0,0 +1,56 @@ +{ + "_from": "@redcode/medium-sdk", + "_id": "@redcode/medium-sdk@0.0.7", + "_inBundle": false, + "_integrity": "sha512-NFCBI15byP62gF0IUolRYL6jxQoZP5e4oNi7XZsLb1IaVqJJD+5BQaqgQy9mlK47e4Vi/hRUBOS4ksp1DZVD2Q==", + "_location": "/@redcode/medium-sdk", + "_phantomChildren": {}, + "_requested": { + "type": "tag", + "registry": true, + "raw": "@redcode/medium-sdk", + "name": "@redcode/medium-sdk", + "escapedName": "@redcode%2fmedium-sdk", + "scope": "@redcode", + "rawSpec": "", + "saveSpec": null, + "fetchSpec": "latest" + }, + "_requiredBy": [ + "#USER", + "/" + ], + "_resolved": "https://registry.npmjs.org/@redcode/medium-sdk/-/medium-sdk-0.0.7.tgz", + "_shasum": "3b4eaaaf2c8c74f8982c376071eb84957c52f964", + "_spec": "@redcode/medium-sdk", + "_where": "/Users/maZahaca/projects/infraway/medium-post-markdown", + "author": { + "name": "Jamie Talbot", + "email": "jamie@jamietalbot.com", + "url": "https://github.com/majelbstoat" + }, + "bugs": { + "url": "https://github.com/medium/medium-sdk-nodejs/issues" + }, + "bundleDependencies": false, + "deprecated": false, + "description": "NodeJS client for the Medium app", + "devDependencies": { + "mocha": "^2.2.5", + "nock": "^2.17", + "should": "^7.1" + }, + "homepage": "https://github.com/medium/medium-sdk-nodejs", + "keywords": [ + "medium", + "api", + "writing" + ], + "main": "index.js", + "name": "@redcode/medium-sdk", + "repository": { + "type": "git", + "url": "git+https://github.com/medium/medium-sdk-nodejs.git" + }, + "version": "0.0.7" +} diff --git a/node_modules/@redcode/medium-sdk/test/mediumClient_test.js b/node_modules/@redcode/medium-sdk/test/mediumClient_test.js new file mode 100644 index 0000000..47e1e08 --- /dev/null +++ b/node_modules/@redcode/medium-sdk/test/mediumClient_test.js @@ -0,0 +1,299 @@ +var medium = require("../") +var nock = require("nock") +var qs = require('querystring') +var should = require("should") +var url = require('url') + + +describe('MediumClient - constructor', function () { + + it('should throw a MediumError when options are undefined', function (done) { + (function () { new medium.MediumClient() }).should.throw(medium.MediumError) + done() + }) + + it('should throw a MediumError when options are empty', function (done) { + (function () { new medium.MediumClient({}) }).should.throw(medium.MediumError) + done() + }) + + it('should throw a MediumError when only clientId is provided', function (done) { + (function () { new medium.MediumClient({clientId: 'xxx'}) }).should.throw(medium.MediumError) + done() + }) + + it('should throw a MediumError when only clientSecret is provided', function (done) { + (function () { new medium.MediumClient({clientSecret: 'yyy'}) }).should.throw(medium.MediumError) + done() + }) + + it('should succeed when both clientId and clientSecret are provided', function (done) { + var client = new medium.MediumClient({clientId: 'xxx', clientSecret: 'yyy'}) + done() + }) +}) + + +describe('MediumClient - methods', function () { + + var clientId = 'xxx' + var clientSecret = 'yyy' + var client + + beforeEach(function () { + client = new medium.MediumClient({clientId: clientId, clientSecret: clientSecret}) + nock.disableNetConnect() + }) + + afterEach(function () { + nock.enableNetConnect(); + delete client + }) + + describe('#setAccessToken', function () { + + it ('sets the access token', function (done) { + var token = "new token" + client.setAccessToken(token) + client._accessToken.should.be.String().and.equal(token) + done() + }) + }) + + describe('#getAuthorizationUrl', function () { + + it ('returns a valid URL for fetching', function (done) { + var state = "state" + var redirectUrl = "https://example.com/callback" + var scope = [medium.Scope.BASIC_PROFILE, medium.Scope.LIST_PUBLICATIONS, medium.Scope.PUBLISH_POST] + var authUrlStr = client.getAuthorizationUrl(state, redirectUrl, scope) + var authUrl = url.parse(authUrlStr, true) + authUrl.protocol.should.equal('https:') + authUrl.hostname.should.equal('medium.com') + authUrl.pathname.should.equal('/m/oauth/authorize') + authUrl.query.should.deepEqual({ + client_id: clientId, + scope: scope.join(','), + response_type: 'code', + state: state, + redirect_uri: redirectUrl + }) + done() + }) + }) + + describe('#exchangeAuthorizationCode', function () { + + it ('makes a request for authorization_code and sets the access token from response', function (done) { + var code = '12345' + var grantType = 'authorization_code' + var redirectUrl = 'https://example.com/callback' + + var requestBody = qs.stringify({ + code: code, + client_id: clientId, + client_secret: clientSecret, + grant_type: grantType, + redirect_uri: redirectUrl + }) + // the response might have other parameters. this test only considers the ones called out + // in the Medium Node SDK documentation + var accessToken = 'abcdef' + var refreshToken = 'ghijkl' + var responseBody = { + access_token: accessToken, + refresh_token: refreshToken + } + var request = nock('https://api.medium.com/', { + 'Content-Type': 'application/x-www-form-urlencoded' + }) + .post('/v1/tokens', requestBody) + .reply(201, responseBody) + + client.exchangeAuthorizationCode(code, redirectUrl, function (err, data) { + if (err) throw err + data.access_token.should.equal(accessToken) + data.refresh_token.should.equal(refreshToken) + done() + }) + request.done() + }) + }) + + describe('#exchangeRefreshToken', function () { + + it ('makes a request for authorization_code and sets the access token from response', function (done) { + var refreshToken = 'fedcba' + var accessToken = 'lkjihg' + + var requestBody = qs.stringify({ + refresh_token: refreshToken, + client_id: clientId, + client_secret: clientSecret, + grant_type: 'refresh_token' + }) + // the response might have other parameters. this test only considers the ones called out + // in the Medium Node SDK documentation + var responseBody = { + access_token: accessToken, + refresh_token: refreshToken + } + var request = nock('https://api.medium.com/', { + 'Content-Type': 'application/x-www-form-urlencoded' + }) + .post('/v1/tokens', requestBody) + .reply(201, responseBody) + + client.exchangeRefreshToken(refreshToken, function (err, data) { + if (err) throw err + data.access_token.should.equal(accessToken) + data.refresh_token.should.equal(refreshToken) + done() + }) + request.done() + }) + }) + + describe('#getUser', function () { + it ('gets the information from expected URL and returns contents of data envelope', function (done) { + var response = { data: 'response data' } + + var request = nock('https://api.medium.com') + .get('/v1/me') + .reply(200, response) + + client.getUser(function (err, data) { + if (err) throw err + data.should.deepEqual(response['data']) + done() + }) + request.done() + }) + }) + + describe('#getPublicationsForUser', function () { + + it ('throws a MediumError when no user ID is provided', function (done) { + (function () { client.getPublicationsForUser({}) }).should.throw(medium.MediumError) + done() + }) + + it ('makes a proper GET request to the Medium API and returns contents of data envelope when valid options are provided', function (done) { + var userId = '123456' + var response = { data: 'response data' } + + var request = nock('https://api.medium.com/') + .get('/v1/users/' + userId + '/publications') + .reply(200, response) + + client.getPublicationsForUser({userId: userId}, function (err, data) { + if (err) throw err + data.should.deepEqual(response['data']) + done() + }) + request.done() + }) + }) + + describe('#getContributorsForPublication', function () { + + it ('throws a MediumError when no publication ID is provided', function (done) { + (function () { client.getContributorsForPublication({}) }).should.throw(medium.MediumError) + done() + }) + + it ('makes a proper GET request to the Medium API and returns contents of data envelope', function (done) { + var options = { publicationId: 'abcdef' } + var response = { data: 'response data' } + var request = nock('https://api.medium.com/') + .get('/v1/publications/' + options.publicationId + '/contributors') + .reply(200, response) + + client.getContributorsForPublication(options, function (err, data) { + if (err) throw err + data.should.deepEqual(response['data']) + done() + }) + request.done() + }) + }) + + describe('#createPost', function () { + + it ('makes a proper POST request to the Medium API and returns contents of data envelope', function (done) { + var options = { + userId: '123456', + title: 'new post title', + content: '

New Post!

', + contentFormat: 'html', + tags: ['js', 'unit tests'], + canonicalUrl: 'http://example.com/new-post', + publishedAt: '2004-02-12T15:19:21+00:00', + publishStatus: 'draft', + license: 'all-rights-reserved' + } + var response = { data: 'response data' } + var request = nock('https://api.medium.com/') + .post('/v1/users/' + options.userId + '/posts', { + title: options.title, + content: options.content, + contentFormat: options.contentFormat, + tags: options.tags, + canonicalUrl: options.canonicalUrl, + publishedAt: options.publishedAt, + publishStatus: options.publishStatus, + license: options.license + }) + .reply(200, response) + + client.createPost(options, function (err, data) { + if (err) throw err + data.should.deepEqual(response['data']) + done() + }) + request.done() + }) + }) + + describe('#createPostInPublication', function () { + + it ('should throw an error when no publication ID is provided', function (done) { + (function () { client.createPostInPublication({}) }).should.throw(medium.MediumError) + done() + }) + + it ('makes a proper POST request to the Medium API and returns contents of data envelope', function (done) { + var options = { + publicationId: 'abcdef', + title: 'new post title', + content: '

New Post!

', + contentFormat: 'html', + tags: ['js', 'unit tests'], + canonicalUrl: 'http://example.com/new-post', + publishedAt: '2004-02-12T15:19:21+00:00', + publishStatus: 'draft', + license: 'all-rights-reserved' + } + var response = { data: 'response data' } + var request = nock('https://api.medium.com/') + .post('/v1/publications/' + options.publicationId + '/posts', { + title: options.title, + content: options.content, + contentFormat: options.contentFormat, + tags: options.tags, + canonicalUrl: options.canonicalUrl, + publishedAt: options.publishedAt, + publishStatus: options.publishStatus, + license: options.license + }) + .reply(200, response) + + client.createPostInPublication(options, function (err, data) { + if (err) throw err + data.should.deepEqual(response['data']) + done() + }) + request.done() + }) + }) +})