diff --git a/.travis.yml b/.travis.yml index b99ebbd9..77b8db8f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,20 +8,14 @@ node_js: notifications: disabled: true -env: - - TEST_DIR=client - - TEST_DIR=server - install: - npm install -g codecov - - npm install --prefix client - - npm install --prefix server + - npm install branches: except: - /^v\d+\.\d+\.\d+$/ script: - - cd $TEST_DIR - npm test - codecov diff --git a/README.md b/README.md index a558bd88..3c657464 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,42 @@ -# smee · [![Build Status](https://img.shields.io/travis/probot/smee/master.svg)](https://travis-ci.org/probot/smee) [![Codecov](https://img.shields.io/codecov/c/github/probot/smee.svg)](https://codecov.io/gh/probot/smee/) +

smee-client

+

Client and CLI for smee.io, a service that delivers webhooks to your local development environment.

+

NPM Build Status Codecov

-**smee** is a web application that receives payloads then sends them, via the [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) API, to other clients. +

Looking for probot/smee.io?

-## How it works +## Installation -1. Go to https://smee.io/new, which will redirect you to a randomly generated channel. You must be using a browser that supports [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events). +Install the client with: -1. Use that new page's URL as your App's Webhook URL +``` +$ npm install -g smee-client +``` + +## Usage -1. Use the [client](./client) to proxy events and send them to a path on your local device, or set it as your Probot App's `WEBHOOK_PROXY_URL` environment variable. +### CLI -1. Watch events come in to the web UI +The `smee` command will forward webhooks from smee.io to your local development environment. -1. Profit! +``` +$ smee +``` -## Motivation +Run `smee --help` for usage. -One of the most cumbersome parts of building a GitHub App with [Probot](https://probot.github.io) is dealing with webhook deliveries. When working locally, for your app to receive webhooks you'd need to expose it to the internet - that's where we've used [localtunnel](https://localtunnel.me). However, it's not very reliable or fast and is more than many apps need to simply collect webhook events. +### Node Client -This also gave us a way to build our own UI around the needs of a Probot App. Together with the GitHub App UI, we now have a better way to get the full picture of what goes on in an app in development. +```js +const SmeeClient = require('smee-client') -## Setup +const smee = new SmeeClient({ + source: 'https://smee.io/abc123', + target: 'http://localhost:3000/events', + logger: console +}) -``` -# Install dependencies -npm install +const events = smee.start() -# Run the server -npm start +// Stop forwarding events +events.close() ``` diff --git a/client/bin/smee.js b/bin/smee.js similarity index 100% rename from client/bin/smee.js rename to bin/smee.js diff --git a/client/README.md b/client/README.md deleted file mode 100644 index 4f82a8c6..00000000 --- a/client/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# smee-client - -Client and CLI for smee.io, a service that delivers webhooks to your local development environment. - -## Installation - -Install the client with: - -``` -$ npm install -g smee-client -``` - -## Usage - -### CLI - -The `smee` command will forward webhooks from smee.io to your local development environment. - -``` -$ smee -``` - -Run `smee --help` for usage. - -### Node Client - -```js -const SmeeClient = require('smee-client') - -const smee = new SmeeClient({ - source: 'https://smee.io/abc123', - target: 'http://localhost:3000/events', - logger: console -}) - -const events = smee.start() - -// Stop forwarding events -events.close() - -``` diff --git a/client/package.json b/client/package.json deleted file mode 100644 index 8d611755..00000000 --- a/client/package.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "name": "smee-client", - "version": "1.1.0", - "description": "Client to proxy webhooks to local host", - "main": "index.js", - "bin": { - "smee": "./bin/smee.js" - }, - "scripts": { - "test": "jest --coverage && standard" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/probot/smee.git" - }, - "author": "", - "license": "ISC", - "bugs": { - "url": "https://github.com/probot/smee/issues" - }, - "homepage": "https://github.com/probot/smee#readme", - "dependencies": { - "commander": "^2.19.0", - "eventsource": "^1.0.7", - "morgan": "^1.9.1", - "superagent": "^5.0.2", - "validator": "^10.11.0" - }, - "devDependencies": { - "@babel/core": "^7.4.0", - "babel-core": "^7.0.0-bridge.0", - "babel-jest": "^24.5.0", - "connect-sse": "^1.2.0", - "jest": "^24.5.0", - "nock": "^10.0.6", - "standard": "^12.0.1", - "supertest": "^4.0.2" - }, - "standard": { - "env": [ - "jest" - ] - } -} diff --git a/client/test/__snapshots__/index.test.js.snap b/client/test/__snapshots__/index.test.js.snap deleted file mode 100644 index 9138b423..00000000 --- a/client/test/__snapshots__/index.test.js.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`client connecting to a channel throws an error if the source is invalid 1`] = `"The provided URL is invalid."`; diff --git a/client/test/index.test.js b/client/test/index.test.js deleted file mode 100644 index e6a8698a..00000000 --- a/client/test/index.test.js +++ /dev/null @@ -1,102 +0,0 @@ -const createServer = require('../../server/server') -const Client = require('..') -const request = require('supertest') -const nock = require('nock') - -// Only allow requests to the proxy server listening on localhost -nock.enableNetConnect('127.0.0.1') - -const logger = { - info: jest.fn(), - error: jest.fn() -} - -describe('client', () => { - let proxy, host, client, channel - - const targetUrl = 'http://example.com/foo/bar' - - beforeEach((done) => { - proxy = createServer().listen(0, () => { - host = `http://127.0.0.1:${proxy.address().port}` - done() - }) - }) - - afterEach(() => { - proxy && proxy.close() - client && client.close() - }) - - describe('connecting to a channel', () => { - beforeEach((done) => { - channel = '/fake-channel' - client = new Client({ - source: `${host}${channel}`, - target: targetUrl, - logger - }).start() - // Wait for event source to be ready - client.addEventListener('ready', () => done()) - }) - - test('throws an error if the source is invalid', async () => { - try { - client = new Client({ - source: 'not-a-real-url', - target: targetUrl, - logger - }).start() - } catch (e) { - expect(e.message).toMatchSnapshot() - } - }) - - test('POST /:channel forwards to target url', async (done) => { - const payload = { payload: true } - - // Expect request to target - const forward = nock('http://example.com').post('/foo/bar', payload).reply(200) - - // Test is done when this is called - client.addEventListener('message', (msg) => { - expect(forward.isDone()).toBe(true) - done() - }) - - // Send request to proxy server - await request(proxy).post(channel).send(payload).expect(200) - }) - - test('POST /:channel forwards query string to target url', async (done) => { - const queryParams = { - param1: 'testData1', - param2: 'testData2' - } - - // Expect request to target - const forward = nock('http://example.com').post('/foo/bar').query(queryParams).reply(200) - - // Test is done when this is called - client.addEventListener('message', (msg) => { - expect(forward.isDone()).toBe(true) - done() - }) - - // Send request to proxy server with query string - await request(proxy).post(channel).query(queryParams).send() - }) - }) - - describe('createChannel', () => { - test('returns a new channel', async () => { - const req = nock('https://smee.io').head('/new').reply(302, '', { - Location: 'https://smee.io/abc123' - }) - - const channel = await Client.createChannel() - expect(channel).toEqual('https://smee.io/abc123') - expect(req.isDone()).toBe(true) - }) - }) -}) diff --git a/client/index.js b/index.js similarity index 100% rename from client/index.js rename to index.js diff --git a/package.json b/package.json index b9f1485b..8d611755 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,44 @@ { - "private": true, + "name": "smee-client", + "version": "1.1.0", + "description": "Client to proxy webhooks to local host", + "main": "index.js", + "bin": { + "smee": "./bin/smee.js" + }, "scripts": { - "postinstall": "npm install --prefix server", - "start": "npm start --prefix server" + "test": "jest --coverage && standard" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/probot/smee.git" + }, + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/probot/smee/issues" + }, + "homepage": "https://github.com/probot/smee#readme", + "dependencies": { + "commander": "^2.19.0", + "eventsource": "^1.0.7", + "morgan": "^1.9.1", + "superagent": "^5.0.2", + "validator": "^10.11.0" + }, + "devDependencies": { + "@babel/core": "^7.4.0", + "babel-core": "^7.0.0-bridge.0", + "babel-jest": "^24.5.0", + "connect-sse": "^1.2.0", + "jest": "^24.5.0", + "nock": "^10.0.6", + "standard": "^12.0.1", + "supertest": "^4.0.2" + }, + "standard": { + "env": [ + "jest" + ] } } diff --git a/server/.babelrc b/server/.babelrc deleted file mode 100644 index 309ab393..00000000 --- a/server/.babelrc +++ /dev/null @@ -1,27 +0,0 @@ -{ - "presets": [ - ["@babel/preset-env", { - "targets": { - "browsers": [ - "last 2 versions", - "ios_saf >= 8", - "ie >= 10", - "chrome >= 49", - "firefox >= 49", - "> 1%" - ] - }, - "debug": false, - "loose": true, - "useBuiltIns": "entry" - }], - "@babel/preset-react" - ], - "plugins": [ - "@babel/plugin-proposal-class-properties", - [ - "@babel/plugin-proposal-object-rest-spread", - { "useBuiltIns": true } - ] - ] -} diff --git a/server/.eslintignore b/server/.eslintignore deleted file mode 100644 index f3da86f1..00000000 --- a/server/.eslintignore +++ /dev/null @@ -1,2 +0,0 @@ -*.min.js -coverage diff --git a/server/.eslintrc b/server/.eslintrc deleted file mode 100644 index fbadf109..00000000 --- a/server/.eslintrc +++ /dev/null @@ -1,11 +0,0 @@ -{ - "parser": "babel-eslint", - "extends": ["standard", "standard-react"], - "rules": { - "jsx-quotes": 0 - }, - "env": { - "jest": true, - "browser": true - } -} \ No newline at end of file diff --git a/server/index.js b/server/index.js deleted file mode 100644 index 120d8c30..00000000 --- a/server/index.js +++ /dev/null @@ -1,5 +0,0 @@ -const app = require('./server')() -const port = process.env.PORT || 3000 -app.listen(port, () => { - console.log('Listening at http://localhost:' + port) -}) diff --git a/server/keep-alive.js b/server/keep-alive.js deleted file mode 100644 index 9623c235..00000000 --- a/server/keep-alive.js +++ /dev/null @@ -1,19 +0,0 @@ -module.exports = class KeepAlive { - constructor (callback, delay) { - this.callback = callback - this.delay = delay - } - - start () { - this.id = setInterval(this.callback, this.delay) - } - - stop () { - clearInterval(this.id) - } - - reset () { - this.stop() - this.start() - } -} diff --git a/server/package.json b/server/package.json deleted file mode 100644 index 3dd3ed71..00000000 --- a/server/package.json +++ /dev/null @@ -1,93 +0,0 @@ -{ - "name": "smee-server", - "version": "0.0.3", - "description": "", - "author": "Jason Etcovitch (https://github.com/JasonEtco)", - "license": "ISC", - "repository": "https://github.com/probot/smee.git", - "scripts": { - "start": "node ./index.js", - "start-dev": "concurrently \"nodemon --ignore src/ ./index.js\" \"webpack -w\"", - "build": "webpack -p", - "test": "jest --coverage && eslint '**/*.js'", - "test:update": "jest -u", - "postinstall": "npm run build" - }, - "dependencies": { - "@babel/core": "^7.2.2", - "@babel/plugin-proposal-class-properties": "^7.2.1", - "@babel/plugin-proposal-object-rest-spread": "^7.2.0", - "@babel/polyfill": "^7.0.0", - "@babel/preset-env": "^7.2.0", - "@babel/preset-react": "^7.0.0", - "@githubprimer/octicons-react": "^8.2.0", - "autoprefixer": "^7.1.6", - "babel-eslint": "^9.0.0", - "babel-loader": "^8.0.4", - "connect-sse": "^1.2.0", - "copy-to-clipboard": "^3.0.8", - "copy-webpack-plugin": "^4.2.0", - "crypto": "^1.0.1", - "css-loader": "^1.0.0", - "eventsource": "^1.0.5", - "express": "^4.16.2", - "express-sslify": "^1.2.0", - "get-value": "^2.0.6", - "glob-all": "^3.1.0", - "helmet": "^3.9.0", - "html-webpack-plugin": "^3.2.0", - "mini-css-extract-plugin": "^0.5.0", - "moment": "^2.19.1", - "moment-timezone": "^0.5.14", - "node-sass": "^4.5.3", - "postcss-loader": "^3.0.0", - "primer-css": "^9.6.0", - "prop-types": "^15.6.0", - "purify-css": "^1.2.5", - "purifycss-webpack": "^0.7.0", - "raven": "^2.6.3", - "react": "^16.0.0", - "react-dom": "^16.0.0", - "react-json-view": "^1.13.2", - "sass-loader": "^7.1.0", - "style-loader": "^0.23.0", - "webpack": "^4.28.3", - "webpack-cli": "^3.2.0" - }, - "devDependencies": { - "babel-core": "^7.0.0-bridge.0", - "babel-jest": "^23.6.0", - "concurrently": "^3.5.0", - "enzyme": "^3.2.0", - "enzyme-adapter-react-16": "^1.1.0", - "eslint": "^5.5.0", - "eslint-config-standard": "^12.0.0", - "eslint-config-standard-react": "^7.0.2", - "eslint-plugin-import": "^2.8.0", - "eslint-plugin-node": "^5.2.1", - "eslint-plugin-promise": "^3.6.0", - "eslint-plugin-react": "^7.5.1", - "eslint-plugin-standard": "^4.0.0", - "jest": "^23.6.0", - "nodemon": "^1.12.1", - "raf": "^3.4.0", - "supertest": "^3.0.0" - }, - "engines": { - "node": "10.x.x" - }, - "jest": { - "setupFiles": [ - "./tests/setup.js" - ], - "testPathIgnorePatterns": [ - "/node_modules/" - ], - "testURL": "https://smee.io/CHANNEL" - }, - "standard": { - "env": [ - "jest" - ] - } -} diff --git a/server/public/favicon.png b/server/public/favicon.png deleted file mode 100644 index 619053a4..00000000 Binary files a/server/public/favicon.png and /dev/null differ diff --git a/server/public/index.html b/server/public/index.html deleted file mode 100644 index 9f9c93c0..00000000 --- a/server/public/index.html +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - smee.io | Webhook payload delivery service - - - - -
-
-
-
-
-
-
-

smee.io

-

Webhook payload delivery service

-

Receives payloads then sends them to your locally running application.

- Start a new channel -
- -
-

If your application needs to respond to webhooks, you'll need some way to expose localhost to the internet. smee.io is a small service that uses Server-Sent Events to proxy payloads from the webhook source, then transmit them to your locally running application.

- -
-
- Webhook Emitter -
-
- localhost -
-
-
-
- -
-
- -

Tell your webhook source to send payloads to your smee.io channel, then either use the smee client or, if you're using Probot to build a GitHub App, just set the environment variable.

-
- - - - - - - diff --git a/server/public/webhooks.html b/server/public/webhooks.html deleted file mode 100644 index 690882db..00000000 --- a/server/public/webhooks.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - smee.io | Webhook deliveries - - - - - -
- - - - - diff --git a/server/server.js b/server/server.js deleted file mode 100644 index e6d86d8d..00000000 --- a/server/server.js +++ /dev/null @@ -1,116 +0,0 @@ -const sse = require('connect-sse')() -const express = require('express') -const crypto = require('crypto') -const bodyParser = require('body-parser') -const EventEmitter = require('events') -const path = require('path') -const Raven = require('raven') - -const KeepAlive = require('./keep-alive') - -// Tiny logger to prevent logs in tests -const log = process.env.NODE_ENV === 'test' ? _ => _ : console.log - -module.exports = (testRoute) => { - const events = new EventEmitter() - const app = express() - const pubFolder = path.join(__dirname, 'public') - - // Used for testing route error handling - if (testRoute) testRoute(app) - - if (process.env.SENTRY_DSN) { - Raven.config(process.env.SENTRY_DSN).install() - app.use(Raven.requestHandler()) - } - - if (process.env.FORCE_HTTPS) { - app.use(require('helmet')()) - app.use(require('express-sslify').HTTPS({ trustProtoHeader: true })) - } - - app.use(bodyParser.json()) - app.use('/public', express.static(pubFolder)) - - app.get('/', (req, res) => { - res.sendFile(path.join(pubFolder, 'index.html')) - }) - - app.get('/new', (req, res) => { - const protocol = req.headers['x-forwarded-proto'] || req.protocol - const host = req.headers['x-forwarded-host'] || req.get('host') - const channel = crypto - .randomBytes(12) - .toString('base64') - .replace(/[+/=]+/g, '') - - res.redirect(307, `${protocol}://${host}/${channel}`) - }) - - app.get('/:channel', (req, res, next) => { - const { channel } = req.params - const bannedChannels = process.env.BANNED_CHANNELS && process.env.BANNED_CHANNELS.split(',') - if (bannedChannels && bannedChannels.includes(channel)) { - return res.status(403).send('Channel has been disabled due to too many connections.') - } - - if (req.accepts('html')) { - log('Client connected to web', channel, events.listenerCount(channel)) - res.sendFile(path.join(pubFolder, 'webhooks.html')) - } else { - next() - } - }, sse, (req, res) => { - const { channel } = req.params - - function send (data) { - res.json(data) - keepAlive.reset() - } - - function close () { - events.removeListener(channel, send) - keepAlive.stop() - log('Client disconnected', channel, events.listenerCount(channel)) - } - - // Setup interval to ping every 30 seconds to keep the connection alive - const keepAlive = new KeepAlive(() => res.json({}, 'ping'), 30 * 1000) - keepAlive.start() - - // Allow CORS - res.setHeader('Access-Control-Allow-Origin', '*') - - // Listen for events on this channel - events.on(channel, send) - - // Clean up when the client disconnects - res.on('close', close) - - res.json({}, 'ready') - - log('Client connected to sse', channel, events.listenerCount(channel)) - }) - - app.post('/:channel', (req, res) => { - events.emit(req.params.channel, { - ...req.headers, - body: req.body, - query: req.query, - timestamp: Date.now() - }) - res.status(200).end() - }) - - // Resend payload via the event emitter - app.post('/:channel/redeliver', (req, res) => { - events.emit(req.params.channel, req.body) - res.status(200).end() - }) - - if (process.env.SENTRY_DSN) { - app.use(Raven.errorHandler()) - } - - return app -} diff --git a/server/src/components/App.js b/server/src/components/App.js deleted file mode 100644 index 75133f1a..00000000 --- a/server/src/components/App.js +++ /dev/null @@ -1,187 +0,0 @@ -import React, { Component } from 'react' -import ListItem from './ListItem' -import get from 'get-value' -import Octicon, { Alert, Pulse, Search, Pin } from '@githubprimer/octicons-react' -import Blank from './Blank' - -export default class App extends Component { - constructor (props) { - super(props) - this.channel = window.location.pathname.substring(1) - this.storageLimit = 30 - - this.clear = this.clear.bind(this) - - this.ref = `smee:log:${this.channel}` - this.pinnedRef = this.ref + ':pinned' - const ref = localStorage.getItem(this.ref) - const pinnedRef = localStorage.getItem(this.pinnedRef) - - this.state = { - log: ref ? JSON.parse(ref) : [], - pinnedDeliveries: pinnedRef ? JSON.parse(pinnedRef) : [], - filter: '', - connection: false - } - - this.togglePinned = this.togglePinned.bind(this) - this.isPinned = this.isPinned.bind(this) - } - - componentDidMount () { - this.setupEventSource() - } - - setupEventSource () { - const url = window.location.pathname - console.log('Connecting to event source:', url) - this.events = new window.EventSource(url) - this.events.onopen = this.onopen.bind(this) - this.events.onmessage = this.onmessage.bind(this) - this.events.onerror = this.onerror.bind(this) - } - - onopen (data) { - this.setState({ - connection: true - }) - } - - onerror (err) { - this.setState({ - connection: false - }) - switch (this.events.readyState) { - case window.EventSource.CONNECTING: - console.log('Reconnecting...', err) - break - case window.EventSource.CLOSED: - console.log('Reinitializing...', err) - this.setupEventSource() - break - } - } - - onmessage (message) { - console.log('received message!') - const json = JSON.parse(message.data) - - // Prevent duplicates in the case of redelivered payloads - const idProp = 'x-github-delivery' - if (json[idProp] === undefined || this.state.log.findIndex(l => l[idProp] === json[idProp]) === -1) { - this.setState({ - log: [json, ...this.state.log] - }, () => { - localStorage.setItem(this.ref, JSON.stringify(this.state.log.slice(0, this.storageLimit))) - }) - } - } - - clear () { - if (confirm('Are you sure you want to clear the delivery log?')) { - console.log('Clearing logs') - const filtered = this.state.log.filter(this.isPinned) - this.setState({ log: filtered }) - if (filtered.length > 0) { - localStorage.setItem(this.ref, JSON.stringify(filtered)) - } else { - localStorage.removeItem(this.ref) - } - } - } - - togglePinned (id) { - const deliveryId = this.state.pinnedDeliveries.indexOf(id) - let pinnedDeliveries - if (deliveryId > -1) { - pinnedDeliveries = [ - ...this.state.pinnedDeliveries.slice(0, deliveryId), - ...this.state.pinnedDeliveries.slice(deliveryId + 1) - ] - } else { - pinnedDeliveries = [...this.state.pinnedDeliveries, id] - } - - this.setState({ pinnedDeliveries }) - localStorage.setItem(this.pinnedRef, JSON.stringify(pinnedDeliveries)) - } - - isPinned (item) { - const id = item['x-github-delivery'] - return this.state.pinnedDeliveries.includes(id) - } - - render () { - const { log, filter, pinnedDeliveries } = this.state - let filtered = log - if (filter) { - filtered = log.filter(l => { - if (filter && filter.includes(':')) { - let [searchString, value] = filter.split(':') - if (!searchString.startsWith('body')) searchString = `body.${searchString}` - console.log(l, searchString, value) - return get(l, searchString) === value - } - return true - }) - } - - const stateString = this.state.connection ? 'Connected' : 'Not Connected' - return ( -
-
-
-

Webhook Deliveries

-
- {this.state.connection - ? - : - } -
-
-
- - {log.length > 0 ? ( -
-
-
- -  get-value syntax - - -
- this.setState({ filter: e.target.value })} - className="input input-lg width-full Box" - /> -
- {pinnedDeliveries.length > 0 && ( - -
Pinned
-
    - {filtered.filter(this.isPinned).map((item, i, arr) => { - const id = item['x-github-delivery'] || item.timestamp - return - })} -
-
- )} -
All
-
    - {filtered.filter(item => !this.isPinned(item)).map((item, i, arr) => { - const id = item['x-github-delivery'] || item.timestamp - return - })} -
-
- ) : } -
- ) - } -} diff --git a/server/src/components/Blank.js b/server/src/components/Blank.js deleted file mode 100644 index 857da09c..00000000 --- a/server/src/components/Blank.js +++ /dev/null @@ -1,74 +0,0 @@ -import React, { Component } from 'react' -import Octicon, { Info } from '@githubprimer/octicons-react' -import CodeExample from './CodeExample' - -export default class Blank extends Component { - render () { - const code = `const SmeeClient = require('smee-client') - -const smee = new SmeeClient({ - source: '${window.location.href}', - target: 'http://localhost:3000/events', - logger: console -}) - -const events = smee.start() - -// Stop forwarding events -events.close()` - - return ( -
-
-
- - -
- e.target.select()} - readOnly - value={window.location.href} - className="form-control input-xl input-block" - /> -

This page will automatically update as things happen.

- -
-
-

Use the CLI

-
-              $ npm install --global smee-client
-            
-

Then the smee command will forward webhooks from smee.io to your local development environment.

-

-              $ smee -u {window.location.href}
-            
- -

For usage info:

-

-              $ smee --help
-            
- -

Use the Node.js client

-
-              $ npm install --save smee-client
-            
-

Then:

- - -

Using Probot's built-in support

-
-              $ npm install --save smee-client
-            
-

Then set the environment variable:

-
-              WEBHOOK_PROXY_URL={window.location.href}
-            
-
-
-
- ) - } -} diff --git a/server/src/components/CodeExample.js b/server/src/components/CodeExample.js deleted file mode 100644 index 5c034e7e..00000000 --- a/server/src/components/CodeExample.js +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react' - -const code = `const SmeeClient = require('smee-client') - -const smee = new SmeeClient({ - source: '${window.location.href}', - target: 'http://localhost:3000/events', - logger: console -}) - -const events = smee.start() - -// Stop forwarding events -events.close()` - -export default function CodeExample () { - return ( -
-  )
-}
diff --git a/server/src/components/EventDescription.js b/server/src/components/EventDescription.js
deleted file mode 100644
index 48de26b9..00000000
--- a/server/src/components/EventDescription.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import React, { Component } from 'react'
-import { string, object, number } from 'prop-types'
-import moment from 'moment-timezone'
-
-export default class EventDescription extends Component {
-  static propTypes = {
-    event: string.isRequired,
-    payload: object.isRequired,
-    timestamp: number.isRequired
-  }
-
-  render () {
-    const { event, payload, timestamp } = this.props
-
-    const formattedTime = moment(timestamp).format('dddd, MMMM Do YYYY, h:mm:ss a')
-    const onARepo = payload.repository && payload.repository.full_name
-    const onRepos = payload.repositories && payload.repositories.every(r => r.full_name)
-
-    return (
-      
-

There was a {event} event received on {formattedTime}.

- {onARepo &&

This event was sent by {payload.repository.full_name}.

} - {onRepos &&

This event was triggered against: {payload.repositories.map(r => {r.full_name})}.

} -
- ) - } -} diff --git a/server/src/components/EventIcon.js b/server/src/components/EventIcon.js deleted file mode 100644 index f9333dd2..00000000 --- a/server/src/components/EventIcon.js +++ /dev/null @@ -1,68 +0,0 @@ -import React, { Component } from 'react' -import { string } from 'prop-types' -import Octicon, { - Comment, - Check, - RepoForked, - Eye, - Checklist, - CloudUpload, - Globe, - Hubot, - Milestone, - Project, - Stop, - Note, - RepoPush, - Package, - GitPullRequest, - Bookmark, - IssueOpened, - IssueClosed -} from '@githubprimer/octicons-react' - -const iconMap = { - push: RepoPush, - pull_request: GitPullRequest, - label: Bookmark, - 'issues.opened': IssueOpened, - 'issues.closed': IssueClosed, - issue_comment: Comment, - status: Check, - fork: RepoForked, - watch: Eye, - check_run: Checklist, - check_suite: Checklist, - deployment: CloudUpload, - deployment_status: CloudUpload, - ping: Globe, - installation: Hubot, - installation_repositories: Hubot, - milestone: Milestone, - project: Project, - project_card: Note, - project_column: Project, - repository_vulnerability_alert: Stop -} - -export default class EventIcon extends Component { - static propTypes = { - action: string, - event: string.isRequired - } - - render () { - const { action, event } = this.props - - let icon - if (action && iconMap[`${event}.${action}`]) { - icon = iconMap[`${event}.${action}`] - } else if (iconMap[event]) { - icon = iconMap[event] - } else { - icon = Package - } - - return - } -} diff --git a/server/src/components/ListItem.js b/server/src/components/ListItem.js deleted file mode 100644 index 986ed10c..00000000 --- a/server/src/components/ListItem.js +++ /dev/null @@ -1,109 +0,0 @@ -import React, { Component } from 'react' -import { object, bool, func } from 'prop-types' -import moment from 'moment' -import ReactJson from 'react-json-view' -import EventIcon from './EventIcon' -import Octicon, { KebabHorizontal, Clippy, Sync, Pin } from '@githubprimer/octicons-react' -import EventDescription from './EventDescription' -import copy from 'copy-to-clipboard' - -export default class ListItem extends Component { - static propTypes = { - item: object.isRequired, - pinned: bool.isRequired, - togglePinned: func.isRequired, - last: bool.isRequired - } - - constructor (props) { - super(props) - this.toggleExpanded = () => this.setState({ expanded: !this.state.expanded }) - this.copy = this.copy.bind(this) - this.redeliver = this.redeliver.bind(this) - this.state = { expanded: false, copied: false, redelivered: false } - } - - copy () { - const { item } = this.props - const event = { event: item['x-github-event'], payload: item.body } - const copied = copy(JSON.stringify(event)) - this.setState({ copied }) - } - - redeliver () { - return window.fetch(`${window.location.pathname}/redeliver`, { - method: 'POST', - body: JSON.stringify(this.props.item), - headers: { - 'Content-Type': 'application/json' - } - }).then(res => { - this.setState({ redelivered: res.status === 200 }) - }) - } - - render () { - const { expanded, copied, redelivered } = this.state - const { item, last, pinned, togglePinned } = this.props - - const event = item['x-github-event'] - const payload = item.body - const id = item['x-github-delivery'] || item.timestamp - - return ( -
  • -
    -
    - -
    - {event} - - -
    - - {expanded && ( -
    -
    -
    -

    Event ID: {id}

    - -
    - -
    - - - -
    -
    -
    -
    -
    Payload
    - -
    -
    - )} -
  • - ) - } -} diff --git a/server/src/main.js b/server/src/main.js deleted file mode 100644 index cb606a98..00000000 --- a/server/src/main.js +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react' -import { render } from 'react-dom' -import './style.scss' -import App from './components/App' - -render(, document.querySelector('.mount')) diff --git a/server/src/style.scss b/server/src/style.scss deleted file mode 100644 index f6d3e4d1..00000000 --- a/server/src/style.scss +++ /dev/null @@ -1,35 +0,0 @@ -@import 'primer-css/index.scss'; - -$blue-650: mix($blue-600, $blue-700, 50%); -$animationSpeed: 1500ms; - -@import 'styles/header-anim'; -@import 'styles/main-anim'; -@import 'styles/github'; - -body { - background-color: $gray-200; -} - -.btn-outline-blue { - background-color: transparent; - color: white; - border-color: white !important; - - &:hover { - color: $blue-500; - background-color: white; - } -} - -.blue-700 { - color: $blue-700; -} - -.input-xl { - font-size: $h1-size; - padding: $spacer-2; - line-height: 1.5; -} - -.octicon { fill: currentColor; } \ No newline at end of file diff --git a/server/src/styles/github.scss b/server/src/styles/github.scss deleted file mode 100644 index 791932b8..00000000 --- a/server/src/styles/github.scss +++ /dev/null @@ -1,99 +0,0 @@ -/* - -github.com style (c) Vasily Polovnyov - -*/ - -.hljs { - display: block; - overflow-x: auto; - padding: 0.5em; - color: #333; - background: #f8f8f8; -} - -.hljs-comment, -.hljs-quote { - color: #998; - font-style: italic; -} - -.hljs-keyword, -.hljs-selector-tag, -.hljs-subst { - color: #333; - font-weight: bold; -} - -.hljs-number, -.hljs-literal, -.hljs-variable, -.hljs-template-variable, -.hljs-tag .hljs-attr { - color: #008080; -} - -.hljs-string, -.hljs-doctag { - color: #d14; -} - -.hljs-title, -.hljs-section, -.hljs-selector-id { - color: #900; - font-weight: bold; -} - -.hljs-subst { - font-weight: normal; -} - -.hljs-type, -.hljs-class .hljs-title { - color: #458; - font-weight: bold; -} - -.hljs-tag, -.hljs-name, -.hljs-attribute { - color: #000080; - font-weight: normal; -} - -.hljs-regexp, -.hljs-link { - color: #009926; -} - -.hljs-symbol, -.hljs-bullet { - color: #990073; -} - -.hljs-built_in, -.hljs-builtin-name { - color: #0086b3; -} - -.hljs-meta { - color: #999; - font-weight: bold; -} - -.hljs-deletion { - background: #fdd; -} - -.hljs-addition { - background: #dfd; -} - -.hljs-emphasis { - font-style: italic; -} - -.hljs-strong { - font-weight: bold; -} diff --git a/server/src/styles/header-anim.scss b/server/src/styles/header-anim.scss deleted file mode 100644 index 969426ec..00000000 --- a/server/src/styles/header-anim.scss +++ /dev/null @@ -1,89 +0,0 @@ - -.header__anim { - position: relative; - width: 128px; - height: 64px; - - &__circle { - border-radius: 50%; - position: absolute; - } - - &__center { - width: 25%; - height: 50%; - top: 25%; - left: 50%; - background-color: $red; - z-index: 4; - animation: pulse $animationSpeed ease-in-out infinite; - - @keyframes pulse { - 0% { transform: translateX(-50%) scale(1); } - 25% { transform: translateX(-50%) scale(1); } - 30% { transform: translateX(-50%) scale(1.1); } - 50% { transform: translateX(-50%) scale(1); } - 65% { transform: translateX(-50%) scale(1.1); } - 70% { transform: translateX(-50%) scale(1); } - 100% { transform: translateX(-50%) scale(1); } - } - } - - &__line { - position: absolute; - left: 0; - width: 100%; - height: 3px; - background-color: $blue-650; - } - - &__dashed-circle { - width: 50%; - height: 100%; - left: 25%; - top: 0; - border: 3px dotted $blue-650; - animation: simpleRotate 10000ms linear infinite; - - @keyframes simpleRotate { - from { transform: rotate(0deg); } - to { transform: rotate(-360deg); } - } - } - - &::before, &::after, &__line { - top: 50%; - transform: translateY(-50%); - } - - &::before, &::after { - content: ''; - position: absolute; - border-radius: 50%; - width: 10%; - height: 20%; - background-color: $blue-300; - z-index: 3; - } - - &::before { left: 0; } - &::after { right: 0; } - - &__payload { - position: absolute; - top: 50%; - left: 0; - width: 10%; - height: 3px; - z-index: 2; - background-color: $blue-800; - animation: payload $animationSpeed linear infinite; - - @keyframes payload { - 0% { transform: translate(0, -50%); } - 30% { transform: translate(500%, -50%); } - 70% { transform: translate(500%, -50%); } - 100% { transform: translate(900%, -50%); } - } - } -} \ No newline at end of file diff --git a/server/src/styles/main-anim.scss b/server/src/styles/main-anim.scss deleted file mode 100644 index 2447638e..00000000 --- a/server/src/styles/main-anim.scss +++ /dev/null @@ -1,101 +0,0 @@ -$bounce: cubic-bezier(0.175, 0.885, 0.32, 1.275); - -.main__anim { - position: relative; - margin: 0 auto; - width: 500px; - height: 250px; - - &__circle { - border-radius: 50%; - position: absolute; - } - - &__center { - width: 25%; - height: 50%; - top: 25%; - left: 50%; - background-color: $red; - z-index: 4; - transform: translateX(-50%); - - &::before, &::after, span::before, span::after { - content: ''; - width: 50%; - position: absolute; - left: 25%; - height: 6px; - border-radius: 2px; - background-color: $red-700; - animation: scaleIn 6000ms ease-in-out infinite; - transform: scaleX(0); - } - - &::before { top: 35%; } - &::after { top: 45%; animation-delay: 200ms } - span::before { top: 55%; animation-delay: 400ms } - span::after { top: 65%; animation-delay: 600ms } - - @keyframes scaleIn { - 0% { transform: scaleX(0); } - 10% { transform: scaleX(1); } - 40% { transform: scaleX(1); } - 50% { transform: scaleX(0); } - 100% { transform: scaleX(0); } - } - } - - &__line { - position: absolute; - left: 0; - width: 100%; - height: 6px; - background-color: $gray-300; - } - - &__left, &__right, &__line { - top: 50%; - transform: translateY(-50%); - } - - &__left, &__right { - content: ''; - position: absolute; - border-radius: 50%; - width: 15%; - height: 30%; - background-color: $blue; - z-index: 3; - - span { - position: absolute; - bottom: -$spacer-2; - font-family: $mono-font; - text-align: center; - transform: translateY(100%); - } - } - - &__left { left: 0; } - &__right { right: 0; } - - &__payload { - position: absolute; - top: 50%; - left: 0; - width: 10%; - height: 6px; - border-radius: 3px; - z-index: 2; - background-color: $blue-800; - animation: payload $animationSpeed linear infinite; - - @keyframes payload { - 0% { transform: translate(0, -50%); } - 30% { transform: translate(500%, -50%); } - 70% { transform: translate(500%, -50%); } - 100% { transform: translate(900%, -50%); } - } - } -} \ No newline at end of file diff --git a/server/tests/App.test.js b/server/tests/App.test.js deleted file mode 100644 index 573835bf..00000000 --- a/server/tests/App.test.js +++ /dev/null @@ -1,179 +0,0 @@ -import React from 'react' -import App from '../src/components/App' -import Blank from '../src/components/Blank' -import { shallow } from 'enzyme' -import issuesOpened from './fixtures/issues.opened.json' -import issuesOpenedTwo from './fixtures/issues.opened2.json' - -describe('', () => { - let localStorage, wrapper, consoleLog - - beforeEach(() => { - localStorage = { - getItem: jest.fn(), - setItem: jest.fn(), - removeItem: jest.fn() - } - - const EventSource = jest.fn() - EventSource.CONNECTING = 0 - EventSource.CLOSED = 2 - - Object.defineProperties(window, { - localStorage: { - value: localStorage, - writable: true - }, - EventSource: { - value: EventSource, - writable: true - }, - location: { - value: { - pathname: '/CHANNEL' - } - } - }) - - console.log = consoleLog = jest.fn() - - wrapper = shallow() - }) - - describe('render', () => { - it('renders the blank page', () => { - expect(wrapper.containsMatchingElement()).toBeTruthy() - }) - - it('renders a list of logs', () => { - wrapper.setState({ log: [issuesOpened] }) - expect(wrapper.find('ListItem').exists()).toBeTruthy() - }) - - it('respects the filter', () => { - wrapper.setState({ log: [issuesOpened, issuesOpenedTwo], filter: 'repository.name:probot' }) - expect(wrapper.find('ListItem').length).toBe(1) - }) - - it('respects the filter if it starts with body', () => { - wrapper.setState({ log: [issuesOpened, issuesOpenedTwo], filter: 'body.repository.name:probot' }) - expect(wrapper.find('ListItem').length).toBe(1) - }) - - it('only filters correct get-value syntax (with :)', () => { - wrapper.setState({ log: [issuesOpened, issuesOpenedTwo], filter: 'hello' }) - expect(wrapper.find('ListItem').length).toBe(2) - }) - - it('updates the App state when the filter input changes', () => { - wrapper.setState({ log: [issuesOpened, issuesOpenedTwo] }) - const input = wrapper.find('input#search') - input.simulate('change', { target: { value: 'hello' } }) - expect(wrapper.state('filter')).toBe('hello') - }) - }) - - describe('onopen', () => { - it('sets the connection state to true', () => { - wrapper.instance().onopen() - expect(wrapper.state('connection')).toBeTruthy() - }) - }) - - describe('onerror', () => { - it('sets the connection state to false', () => { - wrapper.instance().onerror() - expect(wrapper.state('connection')).toBeFalsy() - }) - - it('logs to the console when the state changes to connecting', () => { - wrapper.instance().events.readyState = 0 // CONNECTING - - wrapper.instance().onerror('error') - expect(consoleLog).toHaveBeenCalledWith('Reconnecting...', 'error') - }) - - it('logs to the console when the state changes to closed', () => { - wrapper.instance().events.readyState = 2 // CLOSED - - wrapper.instance().onerror('error') - expect(consoleLog.mock.calls[1]).toEqual(['Reinitializing...', 'error']) - }) - }) - - describe('onmessage', () => { - it('adds the new log to the state and localStorage', () => { - const item = { 'x-github-delivery': 123 } - const message = { data: JSON.stringify(item) } - wrapper.instance().onmessage(message) - - expect(wrapper.state('log')).toEqual([item]) - expect(localStorage.setItem.mock.calls[0][0]).toBe('smee:log:CHANNEL') - expect(localStorage.setItem.mock.calls[0][1]).toMatchSnapshot() - }) - - it('does not add duplicates to the log array or localStorage', () => { - const item = { 'x-github-delivery': 123 } - const message = { data: JSON.stringify(item) } - wrapper.setState({ log: [item] }) - wrapper.instance().onmessage(message) - - expect(wrapper.state('log').length).toBe(1) - expect(localStorage.setItem).not.toHaveBeenCalled() - }) - - it('ignores duplicate check when no x-github-delivery header is supplied', () => { - const item = { body: { value: 'Test body 1' }, timestamp: Date.now() } - const message = { data: JSON.stringify(item) } - wrapper.setState({ log: [item] }) - wrapper.instance().onmessage(message) - - expect(wrapper.state('log').length).toBe(2) - expect(localStorage.setItem).toHaveBeenCalled() - }) - }) - - describe('clear', () => { - beforeEach(() => { - window.confirm = jest.fn(() => true) - const item = { 'x-github-delivery': 123 } - wrapper.setState({ log: [item] }) - }) - - it('clears the log state and localStorage', () => { - wrapper.instance().clear() - expect(wrapper.state('log')).toEqual([]) - expect(localStorage.removeItem).toHaveBeenCalled() - }) - - it('does not clear pinned deliveries', () => { - wrapper.instance().togglePinned(123) - wrapper.instance().clear() - expect(wrapper.state('log')).toMatchSnapshot() - expect(localStorage.setItem.mock.calls[0][1]).toMatchSnapshot() - }) - }) - - describe('togglePinned', () => { - it('adds a pinned item to the array', () => { - wrapper.instance().togglePinned(123) - expect(wrapper.state('pinnedDeliveries')).toEqual([123]) - }) - - it('removes a pinned item from the array', () => { - wrapper.setState({ pinnedDeliveries: [123] }) - wrapper.instance().togglePinned(123) - expect(wrapper.state('pinnedDeliveries')).toEqual([]) - }) - - it('stores the pinnedDeliveries in localStorage', () => { - wrapper.instance().togglePinned(123) - expect(localStorage.setItem.mock.calls[0][0]).toBe('smee:log:CHANNEL:pinned') - expect(localStorage.setItem.mock.calls[0][1]).toMatchSnapshot() - - wrapper.instance().togglePinned(123) - expect(localStorage.setItem.mock.calls[1][0]).toBe('smee:log:CHANNEL:pinned') - expect(localStorage.setItem.mock.calls[1][1]).toMatchSnapshot() - }) - }) -}) diff --git a/server/tests/Blank.test.js b/server/tests/Blank.test.js deleted file mode 100644 index ff6204cd..00000000 --- a/server/tests/Blank.test.js +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react' -import Blank from '../src/components/Blank' -import { shallow } from 'enzyme' - -describe('', () => { - beforeEach(() => { - Object.defineProperties(window, { - location: { - value: { - href: 'https:/smee.io/CHANNEL' - } - } - }) - }) - - describe('render', () => { - it('renders the blank page', () => { - expect().toMatchSnapshot() - }) - - it('selects the input text when focused', () => { - const spy = jest.fn() - const wrapper = shallow() - - wrapper.find('input').simulate('focus', { target: { select: spy } }) - expect(spy).toHaveBeenCalled() - }) - }) -}) diff --git a/server/tests/CodeExample.test.js b/server/tests/CodeExample.test.js deleted file mode 100644 index ebda7fc1..00000000 --- a/server/tests/CodeExample.test.js +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react' -import CodeExample from '../src/components/CodeExample' -import { shallow } from 'enzyme' - -describe('', () => { - describe('render', () => { - it('renders the expected HTML', () => { - const wrapper = shallow() - expect(wrapper.render().text()).toMatchSnapshot() - }) - }) -}) diff --git a/server/tests/EventDescription.test.js b/server/tests/EventDescription.test.js deleted file mode 100644 index a4465f3b..00000000 --- a/server/tests/EventDescription.test.js +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react' -import EventDescription from '../src/components/EventDescription' -import { shallow } from 'enzyme' -import moment from 'moment-timezone' - -describe('', () => { - let props - - beforeEach(() => { - moment.tz.setDefault('UTC') - props = { - event: 'issues', - timestamp: 1513148474751, - payload: { action: 'opened' } - } - }) - - describe('render', () => { - it('renders the correct description', () => { - const wrapper = shallow() - expect(wrapper.find('p').text()).toBe('There was a issues event received on Wednesday, December 13th 2017, 7:01:14 am.') - }) - - it('renders the correct description when on one repo', () => { - const payload = { repository: { full_name: 'probot/probot' } } - const wrapper = shallow() - expect(wrapper.children().length).toBe(2) - expect(wrapper.childAt(1).text()).toBe('This event was sent by probot/probot.') - }) - - it('renders the correct description when on multiple repos', () => { - const payload = { repositories: [ - { full_name: 'probot/probot' }, - { full_name: 'JasonEtco/pizza' } - ] } - const wrapper = shallow() - expect(wrapper.children().length).toBe(2) - expect(wrapper.childAt(1).text()).toBe('This event was triggered against: probot/probotJasonEtco/pizza.') - }) - }) -}) diff --git a/server/tests/EventIcon.test.js b/server/tests/EventIcon.test.js deleted file mode 100644 index e5eae07f..00000000 --- a/server/tests/EventIcon.test.js +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react' -import EventIcon from '../src/components/EventIcon' -import { shallow } from 'enzyme' -import { Globe, IssueOpened, Package } from '@githubprimer/octicons-react' - -describe('', () => { - describe('render', () => { - it('should render the correct octicon', () => { - const wrapper = shallow() - expect(wrapper.find('Octicon').props().icon).toEqual(Globe) - }) - - it('renders the correct octicon if there an action', () => { - const wrapper = shallow() - expect(wrapper.find('Octicon').props().icon).toEqual(IssueOpened) - }) - - it('renders the package octicon if the event is unknown', () => { - const wrapper = shallow() - expect(wrapper.find('Octicon').props().icon).toEqual(Package) - }) - }) -}) diff --git a/server/tests/ListItem.test.js b/server/tests/ListItem.test.js deleted file mode 100644 index 3502902d..00000000 --- a/server/tests/ListItem.test.js +++ /dev/null @@ -1,87 +0,0 @@ -import React from 'react' -import ListItem from '../src/components/ListItem' -import { shallow } from 'enzyme' - -describe('', () => { - let item, el, togglePinned - - beforeEach(() => { - item = { - 'x-github-event': 'issues', - timestamp: 1513148474751, - body: { action: 'opened' } - } - - togglePinned = jest.fn() - - el = shallow() - }) - - describe('redeliver', () => { - it('sets the redelivered state to true', async () => { - const fetch = jest.fn(() => Promise.resolve({ status: 200 })) - Object.defineProperty(window, 'fetch', { value: fetch, writable: true }) - - await el.instance().redeliver() - expect(fetch).toHaveBeenCalled() - expect(el.state('redelivered')).toBeTruthy() - }) - }) - - describe('render', () => { - it('should render with one child', () => { - expect(el.children().length).toBe(1) - }) - - it('should render the expanded markup', () => { - expect(el.children().length).toBe(1) - - el.find('button.ellipsis-expander').simulate('click') - expect(el.children().length).toBe(2) - }) - }) - - describe('copy', () => { - beforeEach(() => { - el.find('button.ellipsis-expander').simulate('click') - }) - - it('changes the button\'s label onClick, then onBlur', async () => { - let btn = el.find('.js-copy-btn') - expect(el.state('copied')).toBeFalsy() - expect(btn.prop('aria-label')).toBe('Copy payload to clipboard') - - el.setState({ copied: true }) - btn = el.find('.js-copy-btn') - expect(btn.prop('aria-label')).toBe('Copied!') - - btn.simulate('focus') - btn.simulate('blur') - expect(el.state('copied')).toBeFalsy() - btn = el.find('.js-copy-btn') - expect(btn.prop('aria-label')).toBe('Copy payload to clipboard') - }) - }) - - describe('redeliver', () => { - beforeEach(() => { - el.find('button.ellipsis-expander').simulate('click') - }) - - it('changes the button\'s label onClick, then onBlur', async () => { - let btn = el.find('.js-redeliver-btn') - expect(el.state('redelivered')).toBeFalsy() - expect(btn.prop('aria-label')).toBe('Redeliver this payload') - - el.setState({ redelivered: true }) - btn = el.find('.js-redeliver-btn') - expect(btn.prop('aria-label')).toBe('Sent!') - - btn.simulate('focus') - btn.simulate('blur') - expect(el.state('redelivered')).toBeFalsy() - btn = el.find('.js-redeliver-btn') - expect(btn.prop('aria-label')).toBe('Redeliver this payload') - }) - }) -}) diff --git a/server/tests/__snapshots__/App.test.js.snap b/server/tests/__snapshots__/App.test.js.snap deleted file mode 100644 index 007e0233..00000000 --- a/server/tests/__snapshots__/App.test.js.snap +++ /dev/null @@ -1,17 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` clear does not clear pinned deliveries 1`] = ` -Array [ - Object { - "x-github-delivery": 123, - }, -] -`; - -exports[` clear does not clear pinned deliveries 2`] = `"[123]"`; - -exports[` onmessage adds the new log to the state and localStorage 1`] = `"[{\\"x-github-delivery\\":123}]"`; - -exports[` togglePinned stores the pinnedDeliveries in localStorage 1`] = `"[123]"`; - -exports[` togglePinned stores the pinnedDeliveries in localStorage 2`] = `"[]"`; diff --git a/server/tests/__snapshots__/Blank.test.js.snap b/server/tests/__snapshots__/Blank.test.js.snap deleted file mode 100644 index 7c94df14..00000000 --- a/server/tests/__snapshots__/Blank.test.js.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` render renders the blank page 1`] = ``; diff --git a/server/tests/__snapshots__/CodeExample.test.js.snap b/server/tests/__snapshots__/CodeExample.test.js.snap deleted file mode 100644 index a2d65bb5..00000000 --- a/server/tests/__snapshots__/CodeExample.test.js.snap +++ /dev/null @@ -1,16 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` render renders the expected HTML 1`] = ` -"const SmeeClient = require('smee-client') - -const smee = new SmeeClient({ - source: 'https://smee.io/CHANNEL', - target: 'http://localhost:3000/events', - logger: console -}) - -const events = smee.start() - -// Stop forwarding events -events.close()" -`; diff --git a/server/tests/__snapshots__/server.test.js.snap b/server/tests/__snapshots__/server.test.js.snap deleted file mode 100644 index 8bd8b280..00000000 --- a/server/tests/__snapshots__/server.test.js.snap +++ /dev/null @@ -1,94 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`server GET / returns the proper HTML 1`] = ` -" - - - - - - smee.io | Webhook payload delivery service - - - - -
    -
    -
    -
    -
    -
    -
    -

    smee.io

    -

    Webhook payload delivery service

    -

    Receives payloads then sends them to your locally running application.

    - Start a new channel -
    - -
    -

    If your application needs to respond to webhooks, you'll need some way to expose localhost to the internet. smee.io is a small service that uses Server-Sent Events to proxy payloads from the webhook source, then transmit them to your locally running application.

    - -
    -
    - Webhook Emitter -
    -
    - localhost -
    -
    -
    -
    - -
    -
    - -

    Tell your webhook source to send payloads to your smee.io channel, then either use the smee client or, if you're using Probot to build a GitHub App, just set the environment variable.

    -
    - - - - - - - -" -`; - -exports[`server GET /:channel returns the proper HTML 1`] = ` -" - - - - - - smee.io | Webhook deliveries - - - - - -
    - - - - - -" -`; diff --git a/server/tests/fixtures/issues.opened.json b/server/tests/fixtures/issues.opened.json deleted file mode 100644 index 0a14153f..00000000 --- a/server/tests/fixtures/issues.opened.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "x-forwarded-proto":"https", - "x-nginx-proxy":"true", - "connection":"close", - "content-length":"7101", - "accept":"*/*", - "user-agent":"GitHub-Hookshot/2e08413", - "x-github-event":"issues", - "x-github-delivery":"123", - "content-type":"application/json", - "x-hub-signature":"123", - "body":{ - "action":"opened", - "issue":{ - "url":"https://api.github.com/repos/JasonEtco/tests/issues/29", - "repository_url":"https://api.github.com/repos/JasonEtco/tests", - "labels_url":"https://api.github.com/repos/JasonEtco/tests/issues/29/labels{/name}", - "comments_url":"https://api.github.com/repos/JasonEtco/tests/issues/29/comments", - "events_url":"https://api.github.com/repos/JasonEtco/tests/issues/29/events", - "html_url":"https://github.com/JasonEtco/tests/issues/29", - "id":291050215, - "number":29, - "title":"asdf", - "user":{ - "login":"JasonEtco", - "id":10660468, - "avatar_url":"https://avatars1.githubusercontent.com/u/10660468?v=4", - "gravatar_id":"", - "url":"https://api.github.com/users/JasonEtco", - "html_url":"https://github.com/JasonEtco", - "followers_url":"https://api.github.com/users/JasonEtco/followers", - "following_url":"https://api.github.com/users/JasonEtco/following{/other_user}", - "gists_url":"https://api.github.com/users/JasonEtco/gists{/gist_id}", - "starred_url":"https://api.github.com/users/JasonEtco/starred{/owner}{/repo}", - "subscriptions_url":"https://api.github.com/users/JasonEtco/subscriptions", - "organizations_url":"https://api.github.com/users/JasonEtco/orgs", - "repos_url":"https://api.github.com/users/JasonEtco/repos", - "events_url":"https://api.github.com/users/JasonEtco/events{/privacy}", - "received_events_url":"https://api.github.com/users/JasonEtco/received_events", - "type":"User", - "site_admin":false - }, - "labels":[ - - ], - "state":"open", - "locked":false, - "assignee":null, - "assignees":[ - - ], - "milestone":null, - "comments":0, - "created_at":"2018-01-24T01:09:57Z", - "updated_at":"2018-01-24T01:09:57Z", - "closed_at":null, - "author_association":"OWNER", - "body":"adsfasdfa" - }, - "repository":{ - "id":110556535, - "name":"tests", - "full_name":"JasonEtco/tests", - "owner":{ - "login":"JasonEtco" - } - } - }, - "timestamp":1517348010998 -} \ No newline at end of file diff --git a/server/tests/fixtures/issues.opened2.json b/server/tests/fixtures/issues.opened2.json deleted file mode 100644 index 26b6ab7c..00000000 --- a/server/tests/fixtures/issues.opened2.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "x-forwarded-proto":"https", - "x-nginx-proxy":"true", - "connection":"close", - "content-length":"7101", - "accept":"*/*", - "user-agent":"GitHub-Hookshot/2e08413", - "x-github-event":"issues", - "x-github-delivery":"123", - "content-type":"application/json", - "x-hub-signature":"123", - "body":{ - "action":"opened", - "issue":{ - "url":"https://api.github.com/repos/JasonEtco/probot/issues/29", - "repository_url":"https://api.github.com/repos/JasonEtco/probot", - "labels_url":"https://api.github.com/repos/JasonEtco/probot/issues/29/labels{/name}", - "comments_url":"https://api.github.com/repos/JasonEtco/probot/issues/29/comments", - "events_url":"https://api.github.com/repos/JasonEtco/probot/issues/29/events", - "html_url":"https://github.com/JasonEtco/probot/issues/29", - "id":291050215, - "number":29, - "title":"asdf", - "user":{ - "login":"JasonEtco", - "id":10660468, - "avatar_url":"https://avatars1.githubusercontent.com/u/10660468?v=4", - "gravatar_id":"", - "url":"https://api.github.com/users/JasonEtco", - "html_url":"https://github.com/JasonEtco", - "followers_url":"https://api.github.com/users/JasonEtco/followers", - "following_url":"https://api.github.com/users/JasonEtco/following{/other_user}", - "gists_url":"https://api.github.com/users/JasonEtco/gists{/gist_id}", - "starred_url":"https://api.github.com/users/JasonEtco/starred{/owner}{/repo}", - "subscriptions_url":"https://api.github.com/users/JasonEtco/subscriptions", - "organizations_url":"https://api.github.com/users/JasonEtco/orgs", - "repos_url":"https://api.github.com/users/JasonEtco/repos", - "events_url":"https://api.github.com/users/JasonEtco/events{/privacy}", - "received_events_url":"https://api.github.com/users/JasonEtco/received_events", - "type":"User", - "site_admin":false - }, - "labels":[ - - ], - "state":"open", - "locked":false, - "assignee":null, - "assignees":[ - - ], - "milestone":null, - "comments":0, - "created_at":"2018-01-24T01:09:57Z", - "updated_at":"2018-01-24T01:09:57Z", - "closed_at":null, - "author_association":"OWNER", - "body":"adsfasdfa" - }, - "repository":{ - "id":110556535, - "name":"probot", - "full_name":"JasonEtco/probot", - "owner":{ - "login":"JasonEtco" - } - } - }, - "timestamp":1517348010998 -} \ No newline at end of file diff --git a/server/tests/server.test.js b/server/tests/server.test.js deleted file mode 100644 index 4814f735..00000000 --- a/server/tests/server.test.js +++ /dev/null @@ -1,130 +0,0 @@ -/** - * @jest-environment node - */ - -const createServer = require('../server') -const request = require('supertest') -const EventSource = require('eventsource') -const Raven = require('raven') - -describe('Sentry tests', () => { - let app, server - beforeEach(() => { - app = createServer() - server = app.listen(0, () => {}) - Raven.captureException = jest.fn() - }) - - it('Starts if SENTRY_DSN is not set', () => { - expect(server).toBeTruthy() - }) - - it('reports errors to Sentry', async () => { - process.env.SENTRY_DSN = 'https://user:pw@sentry.io/1234' - - // Pass a route that errors, just to test it - app = createServer(a => a.get('/not/a/valid/url', () => { throw new Error('test') })) - - await request(app).get('/not/a/valid/url') - expect(Raven.captureException).toHaveBeenCalled() - }) - - it('with an invalid SETRY_DSN', () => { - process.env.SENTRY_DSN = 1234 - expect(createServer).toThrow('Invalid Sentry DSN: 1234') - }) - - afterEach(() => { - server && server.close() - delete process.env.SENTRY_DSN - }) -}) - -describe('server', () => { - let app, server, events, url, channel - - beforeEach((done) => { - channel = '/fake-channel' - app = createServer() - - server = app.listen(0, () => { - url = `http://127.0.0.1:${server.address().port}${channel}` - - // Wait for event source to be ready - events = new EventSource(url) - events.addEventListener('ready', () => done()) - }) - }) - - afterEach(() => { - server && server.close() - events && events.close() - }) - - describe('GET /', () => { - it('returns the proper HTML', async () => { - const res = await request(server).get('/') - expect(res.status).toBe(200) - expect(res.text).toMatchSnapshot() - }) - }) - - describe('GET /new', () => { - it('redirects from /new to /TOKEN', async () => { - const res = await request(server).get('/new') - expect(res.status).toBe(307) - expect(res.headers.location).toMatch(/^http:\/\/127\.0\.0\.1:\d+\/[\w-]+$/) - }) - }) - - describe('GET /:channel', () => { - it('returns the proper HTML', async () => { - const res = await request(server).get(channel) - expect(res.status).toBe(200) - expect(res.text).toMatchSnapshot() - }) - - it('returns a 403 for banned channels', async () => { - process.env.BANNED_CHANNELS = 'hello,imbanned,goodbye' - const res = await request(server).get(`/imbanned`) - expect(res.status).toBe(403) - delete process.env.BANNED_CHANNELS - }) - }) - - describe('events', () => { - it('emits events', async (done) => { - const payload = { payload: true } - - await request(server).post(channel) - .set('X-Foo', 'bar') - .send(payload) - .expect(200) - - events.addEventListener('message', (msg) => { - const data = JSON.parse(msg.data) - expect(data.body).toEqual(payload) - expect(data['x-foo']).toEqual('bar') - - // test is done if all of this gets called - done() - }) - }) - - it('POST /:channel/redeliver re-emits a payload', async (done) => { - const payload = { payload: true } - - await request(server).post(channel + '/redeliver') - .send(payload) - .expect(200) - - events.addEventListener('message', (msg) => { - const data = JSON.parse(msg.data) - expect(data).toEqual(payload) - - // test is done if all of this gets called - done() - }) - }) - }) -}) diff --git a/server/tests/setup.js b/server/tests/setup.js deleted file mode 100644 index b4fa22a6..00000000 --- a/server/tests/setup.js +++ /dev/null @@ -1,11 +0,0 @@ -import 'raf/polyfill' -import Enzyme, { shallow, render, mount } from 'enzyme' -import Adapter from 'enzyme-adapter-react-16' - -// React 16 Enzyme adapter -Enzyme.configure({ adapter: new Adapter() }) - -// Make Enzyme functions available in all test files without importing -global.shallow = shallow -global.render = render -global.mount = mount diff --git a/server/webpack.config.js b/server/webpack.config.js deleted file mode 100644 index af07bd27..00000000 --- a/server/webpack.config.js +++ /dev/null @@ -1,74 +0,0 @@ -const path = require('path') -const webpack = require('webpack') -const autoprefixer = require('autoprefixer') -const glob = require('glob-all') -const PurifyCSSPlugin = require('purifycss-webpack') -const MiniCssExtractPlugin = require('mini-css-extract-plugin') - -const browsers = [ - 'last 2 versions', - 'ios_saf >= 8', - 'ie >= 10', - 'chrome >= 49', - 'firefox >= 49', - '> 1%' -] - -const cfg = { - entry: { - main: path.resolve(__dirname, 'src', 'main.js') - }, - output: { - path: path.join(__dirname, 'public'), - filename: '[name].min.js', - publicPath: '/' - }, - plugins: [ - new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), - // new webpack.optimize.UglifyJsPlugin(), - new MiniCssExtractPlugin({ filename: '[name].min.css' }) - ], - module: { - rules: [{ - test: /\.jsx?$/, - exclude: /node_modules/, - use: { - loader: 'babel-loader' - } - }, { - test: /\.scss$/, - use: [ - MiniCssExtractPlugin.loader, - 'css-loader', - { - loader: 'postcss-loader', - options: { - sourceMap: true, - plugins: () => [autoprefixer(browsers)] - } - }, { - loader: 'sass-loader', - options: { - sourceMap: true, - includePaths: [ - 'node_modules' - ] - } - } - ] - }] - } -} - -if (process.env.NODE_ENV === 'production') { - cfg.plugins.push(new PurifyCSSPlugin({ - minimize: true, - moduleExtensions: ['.js'], - paths: glob.sync([ - path.join(__dirname, 'src', '**/*.js'), - path.join(__dirname, 'public', '*.html') - ]) - })) -} - -module.exports = cfg diff --git a/test/index.test.js b/test/index.test.js new file mode 100644 index 00000000..af1860e1 --- /dev/null +++ b/test/index.test.js @@ -0,0 +1,16 @@ +const Client = require('..') +const nock = require('nock') + +describe('client', () => { + describe('createChannel', () => { + test('returns a new channel', async () => { + const req = nock('https://smee.io').head('/new').reply(302, '', { + Location: 'https://smee.io/abc123' + }) + + const channel = await Client.createChannel() + expect(channel).toEqual('https://smee.io/abc123') + expect(req.isDone()).toBe(true) + }) + }) +})