UNPKG

stitch-ui

Version:

248 lines (221 loc) 7.44 kB
/* global afterEach, beforeEach */ /* eslint-disable import/no-extraneous-dependencies */ import util from "util"; import { Admin } from "mongodb-stitch"; import { createStore, applyMiddleware } from "redux"; import { OrderedMap } from "immutable"; import thunk from "redux-thunk"; import colors from "colors/safe"; import sinon from "sinon"; import bson from "bson"; import crypto from "crypto"; import { createMemoryHistory } from "history"; import { MongoClient } from "mongodb"; import adminConsole from "./reducers"; import { setClient, setHistory, setSettings, getUserProfile } from "./actions"; import Confirm from "./core/confirm"; const testSalt = process.env.STITCH_TEST_SALT || "DQOWene1723baqD!_@#"; const randomString = length => { const chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; let result = ""; for (let i = length; i > 0; i -= 1) { result += chars[Math.floor(Math.random() * chars.length)]; } return result; }; export const timeout = ms => new Promise(resolve => setTimeout(resolve, ms)); export const hashValue = (key, salt) => new Promise((resolve, reject) => { crypto.pbkdf2(key, salt, 4096, 32, "sha256", (err, outputKey) => { if (err) { reject(err); } resolve(outputKey); }); }); // Lets you subscribe to specific actions on a redux store so that you can // respond to them by triggering a callback. // Probably slow so shouldn't be used outside of testing. class ReduxActionListener { constructor() { this.subscriptions = {}; } // Creates a subscription that triggers 'callback' to be called // each time the given action type is received. // Returns a function which can be called to unsubscribe. subscribe(actionType, callback) { let subs = this.subscriptions[actionType] || new OrderedMap(); const key = Math.random(); subs = subs.set(key, callback); this.subscriptions[actionType] = subs; return () => { let updatedSubs = this.subscriptions[actionType] || new OrderedMap(); updatedSubs = updatedSubs.remove(key); if (updatedSubs.size === 0) { delete this.subscriptions[actionType]; return; } this.subscriptions[actionType] = updatedSubs; }; } reset() { this.subscriptions = {}; } createMiddleware() { return store => next => action => { if ( Object.prototype.hasOwnProperty.call(this.subscriptions, action.type) ) { next(action); const newState = store.getState(); this.subscriptions[action.type].toArray().forEach(callback => { callback(action, newState); }); } next(action); }; } } export const generateTestUser = async mongoUrl => { const rootId = bson.ObjectId("000000000000000000000000"); const apiKeyId = bson.ObjectId(); const userId = bson.ObjectId().toHexString(); const groupId = bson.ObjectId().toHexString(); const testUser = { userId, domainId: rootId, identities: [{ id: apiKeyId.toHexString(), provider: "api/key" }], roles: [{ roleName: "groupOwner", groupId }] }; const key = randomString(64); const hashedKey = (await hashValue(key, testSalt)).toString("hex"); // TODO: Attaching this to the user causes tests to fail for creating API keys const apiKeyUserId = bson.ObjectId().toHexString(); const testAPIKey = { _id: apiKeyId, domainId: rootId, userId: apiKeyUserId, appId: rootId, key, hashedKey, name: apiKeyId.toString(), disabled: false, visible: true }; const testPassword = { salt: Buffer("testsalt"), hashedPassword: Buffer.from( "ZjVlNDhhZjM4NTM3ZjA4YjBhNmYxNGNkZTlmOGU4YTJiZjY5YjVhYTA4ZTg2ODc3MjU5ODg1YWYxMmEzMGNjZg==", "base64" ), domainId: bson.ObjectID("000000000000000000000000"), loginIds: [ { id_type: "email", id: "unique_user@domain.com", confirmed: true } ] }; const testGroup = { domainId: rootId, groupId }; const db = await MongoClient.connect(mongoUrl); await db .collection("passwords") .update( { "loginIds.id": "unique_user@domain.com" }, { $set: testPassword }, { upsert: true } ); await db.collection("users").insert(testUser); await db.collection("apiKeys").insert(testAPIKey); await db.collection("groups").insert(testGroup); await db.close(); return { user: testUser, apiKey: testAPIKey, group: testGroup }; }; const testDebugLoggingMiddleware = () => next => action => { const tempAction = { ...action }; delete tempAction.type; // eslint-disable-next-line no-console console.log(colors.green(action.type), colors.grey(util.inspect(tempAction))); next(action); }; const simulateChange = (dom, selector, value, index) => { let elem = dom.find(selector); if (index !== undefined) { elem = elem.at(index); } return elem.simulate("change", { target: { value } }); }; export const changeInput = (dom, inputName, value, index) => simulateChange(dom, `input[name='${inputName}']`, value, index); export const changeTextArea = (dom, inputName, value, index) => simulateChange(dom, `textarea[name='${inputName}']`, value, index); export function noConsoleErrorsAllowed() { beforeEach(() => { sinon.stub(console, "error"); }); afterEach(() => { sinon.assert.notCalled(console.error); // eslint-disable-line no-console console.error.restore(); // eslint-disable-line no-console }); } export const testSetup = async verbose => { let mongoUrl = "mongodb://localhost:26000/auth"; if (process.env.STITCH_MONGO_TEST_URL) { mongoUrl = process.env.STITCH_MONGO_TEST_URL; } let adminApiUrl = "http://localhost:9090"; if (process.env.STITCH_API_TEST_URL) { adminApiUrl = process.env.STITCH_API_TEST_URL; } const testAuth = await generateTestUser(mongoUrl); const actionSub = new ReduxActionListener(); const middlewares = [thunk, actionSub.createMiddleware()]; if (verbose) { middlewares.push(testDebugLoggingMiddleware); } const store = createStore(adminConsole, applyMiddleware(...middlewares)); const admin = new Admin(adminApiUrl); await admin.client.authenticate("apiKey", testAuth.apiKey.key); store.dispatch(setClient(admin)); store.dispatch(setSettings({ apiUrl: adminApiUrl })); await store.dispatch(getUserProfile); const history = createMemoryHistory(); store.dispatch(setHistory(history)); return { store, user: testAuth.user, actionSub, apiKey: testAuth.apiKey, admin, groupId: testAuth.group.groupId }; }; /** * Stub the Confirm modal (which replaces the default browser confirm dialog) to * return either true or false (OK or CANCEL, respectively). * * @param {Boolean} result The mocked result of the confirm modal/dialog */ export const stubConfirmation = result => { const confirmStub = sinon.stub(Confirm, "confirm"); confirmStub.resolves(result); }; /** * Mocks relevant missing browser elements to prevent brace from * cluttering test output with warnings. * * @param {Object} window the global window object created by jsdom */ export function braceCompatShim(window) { window.URL.createObjectURL = () => "fake-url"; // eslint-disable-line class Worker { postMessage() {} // eslint-disable-line class-methods-use-this terminate() {} // eslint-disable-line class-methods-use-this } global.Worker = Worker; }