stitch-ui
Version:
248 lines (221 loc) • 7.44 kB
JavaScript
/* 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;
}