diff --git a/.circleci/config.yml b/.circleci/config.yml index 4fe2e006b5..1d65c3013d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -42,6 +42,7 @@ aliases: | cp now-secrets.example.json now-secrets.json yarn run build:web + yarn run build:hyperion - &build-api name: Build API @@ -54,7 +55,7 @@ aliases: - &start-web name: Start web client in the background - command: yarn run dev:web + command: yarn run start:test background: true defaults: &defaults @@ -105,15 +106,18 @@ jobs: - run: *setup-and-build-web - run: *build-api - run: *start-api + - run: ./node_modules/.bin/wait-on http://localhost:3001 - run: *start-web - # Wait for the API and webserver to start - - run: ./node_modules/.bin/wait-on http://localhost:3000 http://localhost:3001 + - run: ./node_modules/.bin/wait-on http://localhost:3006 - run: name: Run Unit Tests command: yarn run test:ci - run: name: Build desktop apps command: yarn run build:desktop + - run: + name: Install Cypress + command: yarn run cypress:install - run: name: Run E2E Tests command: test $CYPRESS_RECORD_KEY && yarn run test:e2e -- --record || yarn run test:e2e diff --git a/api/index.js b/api/index.js index dfdb629773..5df064d8ef 100644 --- a/api/index.js +++ b/api/index.js @@ -67,11 +67,7 @@ app.use( ); app.use('/', (req: express$Request, res: express$Response) => { - res.redirect( - process.env.NODE_ENV === 'production' && !process.env.FORCE_DEV - ? 'https://spectrum.chat' - : 'http://localhost:3000' - ); + res.send('API active. Did you mean to query /api?'); }); import type { Loader } from './loaders/types'; diff --git a/api/routes/api/email.js b/api/routes/api/email.js index 9a12adc28e..ad41123c9d 100644 --- a/api/routes/api/email.js +++ b/api/routes/api/email.js @@ -20,6 +20,11 @@ import { } from '../../models/community'; import { getChannelsByCommunity } from '../../models/channel'; +const BASE_URL = + IS_PROD && !IS_TESTING + ? 'https://spectrum.chat' + : IS_TESTING ? 'http://localhost:3006' : 'http://localhost:3000'; + // $FlowIssue emailRouter.get('/unsubscribe', (req, res) => { const { token } = req.query; @@ -172,14 +177,7 @@ emailRouter.get('/validate', (req, res) => { if (communityId) { try { return updateCommunityAdministratorEmail(communityId, email, userId).then( - community => - IS_PROD - ? res.redirect( - `https://spectrum.chat/${community.slug}/settings/billing` - ) - : res.redirect( - `http://localhost:3000/${community.slug}/settings/billing` - ) + community => `${BASE_URL}/${community.slug}/settings/billing` ); } catch (err) { console.error(err); @@ -194,14 +192,7 @@ emailRouter.get('/validate', (req, res) => { // and send a database request to update the user record with this email try { return updateUserEmail(userId, email).then( - user => - IS_PROD - ? res.redirect( - `https://spectrum.chat/users/${user.username}/settings` - ) - : res.redirect( - `http://localhost:3000/users/${user.username}/settings` - ) + user => `${BASE_URL}/users/${user.username}/settings` ); } catch (err) { console.error(err); @@ -221,14 +212,14 @@ if (IS_TESTING) { 'briandlovin@gmail.com', BRIAN_ID ).then(() => - res.redirect('http://localhost:3000/payments/settings/billing') + res.redirect('http://localhost:3006/payments/settings/billing') ); }); // $FlowIssue emailRouter.get('/validate/test-payments/reset', (req, res) => { return resetCommunityAdministratorEmail(PAYMENTS_COMMUNITY_ID).then(() => - res.redirect('http://localhost:3000/payments/settings/billing') + res.redirect('http://localhost:3006/payments/settings/billing') ); }); } diff --git a/config-overrides.js b/config-overrides.js index 4ec428f269..3917f336ef 100644 --- a/config-overrides.js +++ b/config-overrides.js @@ -108,6 +108,7 @@ module.exports = function override(config, env) { caches: process.env.NODE_ENV === 'development' ? {} : 'all', externals, autoUpdate: true, + rewrites: arg => arg, ServiceWorker: { entry: './public/push-sw.js', events: true, diff --git a/cypress.json b/cypress.json index 77ee091c6c..b174e258c5 100644 --- a/cypress.json +++ b/cypress.json @@ -1,9 +1,11 @@ { - "baseUrl": "http://localhost:3000", + "baseUrl": "http://localhost:3006", "viewportWidth": 1300, "defaultCommandTimeout": 20000, + "chromeWebSecurity": false, "blacklistHosts": [ - "*.google-analytics.com" + "*.google-analytics.com", + "*.amplitude.com" ], "env": { "DEBUG": "src*,testing*,build*" diff --git a/cypress/integration/community_settings_billing_spec.js b/cypress/integration/community_settings_billing_spec.js index 523f0c3d60..5e00395f25 100644 --- a/cypress/integration/community_settings_billing_spec.js +++ b/cypress/integration/community_settings_billing_spec.js @@ -14,7 +14,7 @@ const channels = data.channels.filter( ); const verify = () => { - cy.visit(`http://localhost:3001/api/email/validate/test-payments/verify`); + cy.visit(`http://localhost:3006/api/email/validate/test-payments/verify`); }; describe('Community settings billing tab', () => { @@ -269,7 +269,7 @@ describe('Community settings billing tab', () => { // as the last test, reset the administrator email describe('should reset the administration email', () => { it('should reset email addres', () => { - cy.visit(`http://localhost:3001/api/email/validate/test-payments/reset`); + cy.visit(`http://localhost:3006/api/email/validate/test-payments/reset`); }); }); diff --git a/cypress/integration/navbar_spec.js b/cypress/integration/navbar_spec.js index e0cd34e776..a36fd4d437 100644 --- a/cypress/integration/navbar_spec.js +++ b/cypress/integration/navbar_spec.js @@ -117,6 +117,7 @@ describe('Navbar logged in', () => { describe('Navbar logged out', () => { beforeEach(() => { + cy.auth(null); cy.visit(`/`); }); diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js index dffed2532f..74ff371a2e 100644 --- a/cypress/plugins/index.js +++ b/cypress/plugins/index.js @@ -10,8 +10,21 @@ // This function is called when a project is opened or re-opened (e.g. due to // the project's config changing) +const setup = require('../../shared/testing/setup'); +const teardown = require('../../shared/testing/teardown'); module.exports = (on, config) => { // `on` is used to hook into various events Cypress emits // `config` is the resolved Cypress config + on('task', { + resetdb() { + return ( + teardown() + .then(setup) + // NOTE: This is required, Cypress scripts have to explicity return `null` + // to be considered "done". (instead of implicitly `undefined`) + .then(() => null) + ); + }, + }); }; diff --git a/cypress/support/commands.js b/cypress/support/commands.js index a53d20718b..5750605754 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -10,6 +10,10 @@ import { encode } from '../../api/utils/base64'; Cypress.Commands.add('auth', userId => { + if (userId === null) { + localStorage.removeItem('session'); + return cy.clearCookie('session'); + } localStorage.setItem( 'spectrum', JSON.stringify({ currentUser: { id: userId } }) @@ -24,15 +28,6 @@ Cypress.Commands.add('auth', userId => { ); }); -Cypress.Commands.add('resetdb', () => { - cy.exec( - `node -e "const teardown = require('./shared/testing/teardown.js')().then(() => process.exit())"` - ); - cy.exec( - `node -e "const setup = require('./shared/testing/setup.js')().then(() => process.exit())"` - ); -}); - Cypress.Commands.overwrite('type', (originalFn, $elem, text, options) => { const textarea = $elem[0]; // If it's a DraftJS editor, simulate text events diff --git a/cypress/support/index.js b/cypress/support/index.js index f90f1de641..2a321c6f2f 100644 --- a/cypress/support/index.js +++ b/cypress/support/index.js @@ -17,11 +17,11 @@ import './commands'; before(() => { - cy.resetdb(); + cy.task('resetdb'); cy.clearLocalStorage(); }); beforeEach(() => { - cy.resetdb(); + cy.task('resetdb'); cy.clearLocalStorage(); }); diff --git a/hyperion/cache.js b/hyperion/cache.js index a4b3126796..e328d4d201 100644 --- a/hyperion/cache.js +++ b/hyperion/cache.js @@ -4,7 +4,7 @@ import redis from './redis'; const debug = require('debug')('hyperion:cache'); if (process.env.DISABLE_CACHE) { - console.log( + console.warn( 'Cache disabled, either unset DISABLE_CACHE env variable or run in production mode to enable.' ); } @@ -14,13 +14,13 @@ const cache = ( res: express$Response, next: express$NextFunction ) => { - res.setHeader('Content-Type', 'text/html; charset=utf-8'); - if (process.env.DISABLE_CACHE) return next(); if (req.method !== 'GET') { debug(`${req.method} request came in, not caching`); return next(); } + + res.setHeader('Content-Type', 'text/html; charset=utf-8'); if (req.user) { req.user.id && typeof req.user.id === 'string' && diff --git a/hyperion/index.js b/hyperion/index.js index 3b3514d652..93cf4552c2 100644 --- a/hyperion/index.js +++ b/hyperion/index.js @@ -40,42 +40,6 @@ if (process.env.NODE_ENV === 'production' && !process.env.FORCE_DEV) { import cors from 'shared/middlewares/cors'; app.use(cors); -// Redirect requests to /api and /auth to the production API -// This allows deploy previews to work, as this route would only be called -// if there's no path alias in Now for hyperionurl.com/api, which would only -// happen on deploy previews -app.use('/api', (req: express$Request, res: express$Response) => { - const redirectUrl = `${req.baseUrl}${req.path}`; - res.redirect( - req.method === 'POST' || req.xhr ? 307 : 301, - `https://spectrum.chat${redirectUrl}` - ); -}); - -app.use('/auth', (req: express$Request, res: express$Response) => { - const redirectUrl = `${req.baseUrl}${req.path}`; - res.redirect( - req.method === 'POST' || req.xhr ? 307 : 301, - `https://spectrum.chat${redirectUrl}` - ); -}); - -app.use('/websocket', (req: express$Request, res: express$Response) => { - const redirectUrl = `${req.baseUrl}${req.path}`; - res.redirect( - req.method === 'POST' || req.xhr ? 307 : 301, - `https://spectrum.chat${redirectUrl}` - ); -}); - -// In development the Webpack HMR server requests /sockjs-node constantly, -// so let's patch that through to it! -if (process.env.NODE_ENV === 'development') { - app.use('/sockjs-node', (req: express$Request, res: express$Response) => { - res.redirect(301, `http://localhost:3000${req.path}`); - }); -} - import cookieParser from 'cookie-parser'; app.use(cookieParser()); @@ -106,6 +70,57 @@ app.use(passport.session()); import threadParamRedirect from 'shared/middlewares/thread-param'; app.use(threadParamRedirect); +const BASE_URI = + process.env.NODE_ENV === 'testing' || process.env.TEST_DB + ? 'http://localhost:3001' + : 'https://spectrum.chat'; + +// Redirect requests to /api and /auth to the production API +// This allows deploy previews to work, as this route would only be called +// if there's no path alias in Now for hyperionurl.com/api, which would only +// happen on deploy previews +app.use('/api', (req: express$Request, res: express$Response) => { + const redirectUrl = `${req.baseUrl}${req.path}`; + if (req.headers['content-type'] === 'application/json') { + return fetch(`${BASE_URI}${redirectUrl}`, { + method: req.method, + headers: { + cookie: req.headers.cookie, + 'content-type': req.headers['content-type'], + }, + body: JSON.stringify(req.body), + }) + .then(res => res.json()) + .then(json => res.send(json)); + } + + return res.redirect(`${BASE_URI}${redirectUrl}`); +}); + +app.use('/auth', (req: express$Request, res: express$Response) => { + const redirectUrl = `${req.baseUrl}${req.path}`; + res.redirect( + req.method === 'POST' || req.xhr ? 307 : 301, + `${BASE_URI}${redirectUrl}` + ); +}); + +app.use('/websocket', (req: express$Request, res: express$Response) => { + const redirectUrl = `${req.baseUrl}${req.path}`; + res.redirect( + req.method === 'POST' || req.xhr ? 307 : 301, + `${BASE_URI}${redirectUrl}` + ); +}); + +// In development the Webpack HMR server requests /sockjs-node constantly, +// so let's patch that through to it! +if (process.env.NODE_ENV === 'development') { + app.use('/sockjs-node', (req: express$Request, res: express$Response) => { + res.redirect(301, `http://localhost:3000${req.path}`); + }); +} + // Static files // This route handles the case where our ServiceWorker requests main.asdf123.js, but // we've deployed a new version of the app so the filename changed to main.dfyt975.js diff --git a/package.json b/package.json index cdb2e32574..e96ede75c2 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "cors": "^2.8.3", "cryptr": "^3.0.0", "css.escape": "^1.5.1", - "cypress": "^2.1.0", + "cypress": "^3.0.1", "dataloader": "^1.3.0", "debounce": "^1.1.0", "debug": "^2.6.8", @@ -258,7 +258,9 @@ "test:e2e": "cypress run", "prestart:api:test": "node -e \"require('./shared/testing/setup.js')().then(() => process.exit())\"", "start:api:test": "TEST_DB=true FORCE_DEV=true DEBUG=api*,shared* forever build-api/main.js", + "start:test": "DISABLE_CACHE=true TEST_DB=true FORCE_DEV=true DEBUG=hyperion*,shared* forever build-hyperion/main.js", "cypress:open": "cypress open", + "cypress:install": "cypress install", "lint": "eslint .", "flow": "flow", "db:migrate": "npm run rethinkdb:migrate -- up", diff --git a/shared/graphql/constants.js b/shared/graphql/constants.js index b43dd7224f..a2ce833ba1 100644 --- a/shared/graphql/constants.js +++ b/shared/graphql/constants.js @@ -1,10 +1,12 @@ // @flow // The constants for the GraphQL client on the web -export const IS_PROD = - process.env.NODE_ENV === 'production' && !process.env.FORCE_DEV; +export const IS_PROD = process.env.NODE_ENV === 'production'; // In production the API is at the same URL, in development it's at a different port export const API_URI = IS_PROD ? '/api' : 'http://localhost:3001/api'; -export const WS_URI = IS_PROD - ? `wss://${window.location.host}/websocket` - : 'ws://localhost:3001/websocket'; +// If the production build is running locally connect to the websocket at localhost:3001 +// else connect at thisurl.whatever/websocket +export const WS_URI = + IS_PROD && window.location.host !== 'localhost:3006' + ? `wss://${window.location.host}/websocket` + : 'ws://localhost:3001/websocket'; diff --git a/shared/middlewares/thread-param.js b/shared/middlewares/thread-param.js index cb59d78dda..5dc3fda46f 100644 --- a/shared/middlewares/thread-param.js +++ b/shared/middlewares/thread-param.js @@ -1,15 +1,15 @@ -// Redirect any route ?thread= to /thread/ - -const threadParamRedirect = (req, res, next) => { - // Redirect /?t=asdf123 if the user isn't logged in - if (req.query.t) { - res.redirect(`/thread/${req.query.t}`); - // Redirect /anything?thread=asdf123 - } else if (req.query.thread) { - res.redirect(`/thread/${req.query.thread}`); - } else { - next(); - } -}; - -export default threadParamRedirect; +// Redirect any route ?thread= to /thread/ + +const threadParamRedirect = (req, res, next) => { + // Redirect /?t=asdf123 if the user isn't logged in + if (req.query.t && !req.user) { + res.redirect(`/thread/${req.query.t}`); + // Redirect /anything?thread=asdf123 + } else if (req.query.thread) { + res.redirect(`/thread/${req.query.thread}`); + } else { + next(); + } +}; + +export default threadParamRedirect; diff --git a/src/components/composer/index.js b/src/components/composer/index.js index 6265f9139e..2df60db7e3 100644 --- a/src/components/composer/index.js +++ b/src/components/composer/index.js @@ -706,7 +706,7 @@ class ComposerWithData extends Component { Cancel