mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) disentangle some server tests, release to core, add GRIST_PROXY_AUTH_HEADER test
Summary: This shuffles some server tests to make them available in grist-core, and adds a test for the `GRIST_PROXY_AUTH_HEADER` feature added in https://github.com/gristlabs/grist-core/pull/165 It includes a fix for a header normalization issue for websocket connections. Test Plan: added test Reviewers: georgegevoian Reviewed By: georgegevoian Differential Revision: https://phab.getgrist.com/D3326
This commit is contained in:
66
test/server/customUtil.ts
Normal file
66
test/server/customUtil.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import {getAppRoot} from 'app/server/lib/places';
|
||||
import {fromCallback} from 'bluebird';
|
||||
import * as express from 'express';
|
||||
import * as http from 'http';
|
||||
import {AddressInfo, Socket} from 'net';
|
||||
import * as path from 'path';
|
||||
import {fixturesRoot} from 'test/server/testUtils';
|
||||
|
||||
export interface Serving {
|
||||
url: string;
|
||||
shutdown: () => void;
|
||||
}
|
||||
|
||||
|
||||
// Adds static files from a directory.
|
||||
// By default exposes /fixture/sites
|
||||
export function addStatic(app: express.Express, rootDir?: string) {
|
||||
// mix in a copy of the plugin api
|
||||
app.use(/^\/(grist-plugin-api.js)$/, (req, res) =>
|
||||
res.sendFile(req.params[0], {root:
|
||||
path.resolve(getAppRoot(), "static")}));
|
||||
app.use(express.static(rootDir || path.resolve(fixturesRoot, "sites"), {
|
||||
setHeaders: (res) => {
|
||||
res.set("Access-Control-Allow-Origin", "*");
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// Serve from a directory.
|
||||
export async function serveStatic(rootDir: string): Promise<Serving> {
|
||||
return serveSomething(app => addStatic(app, rootDir));
|
||||
}
|
||||
|
||||
// Serve a string of html.
|
||||
export async function serveSinglePage(html: string): Promise<Serving> {
|
||||
return serveSomething(app => {
|
||||
app.get('', (req, res) => res.send(html));
|
||||
});
|
||||
}
|
||||
|
||||
export function serveCustomViews(): Promise<Serving> {
|
||||
return serveStatic(path.resolve(fixturesRoot, "sites"));
|
||||
}
|
||||
|
||||
export async function serveSomething(setup: (app: express.Express) => void, port= 0): Promise<Serving> {
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
await fromCallback((cb: any) => server.listen(port, cb));
|
||||
|
||||
const connections = new Set<Socket>();
|
||||
server.on('connection', (conn) => {
|
||||
connections.add(conn);
|
||||
conn.on('close', () => connections.delete(conn));
|
||||
});
|
||||
|
||||
function shutdown() {
|
||||
server.close();
|
||||
for (const conn of connections) { conn.destroy(); }
|
||||
}
|
||||
|
||||
port = (server.address() as AddressInfo).port;
|
||||
app.set('port', port);
|
||||
setup(app);
|
||||
const url = `http://localhost:${port}`;
|
||||
return {url, shutdown};
|
||||
}
|
||||
243
test/server/docTools.ts
Normal file
243
test/server/docTools.ts
Normal file
@@ -0,0 +1,243 @@
|
||||
import {getDocWorkerMap} from 'app/gen-server/lib/DocWorkerMap';
|
||||
import {ActiveDoc} from 'app/server/lib/ActiveDoc';
|
||||
import {DummyAuthorizer} from 'app/server/lib/Authorizer';
|
||||
import {create} from 'app/server/lib/create';
|
||||
import {DocManager} from 'app/server/lib/DocManager';
|
||||
import {DocSession, makeExceptionalDocSession} from 'app/server/lib/DocSession';
|
||||
import {DocStorageManager} from 'app/server/lib/DocStorageManager';
|
||||
import {GristServer} from 'app/server/lib/GristServer';
|
||||
import {IDocStorageManager} from 'app/server/lib/IDocStorageManager';
|
||||
import {getAppRoot} from 'app/server/lib/places';
|
||||
import {PluginManager} from 'app/server/lib/PluginManager';
|
||||
import {createTmpDir as createTmpUploadDir, FileUploadInfo, globalUploadSet} from 'app/server/lib/uploads';
|
||||
import * as testUtils from 'test/server/testUtils';
|
||||
|
||||
import {assert} from 'chai';
|
||||
import * as fse from 'fs-extra';
|
||||
import {tmpdir} from 'os';
|
||||
import * as path from 'path';
|
||||
import * as tmp from 'tmp';
|
||||
|
||||
tmp.setGracefulCleanup();
|
||||
|
||||
// it is sometimes useful in debugging to turn off automatic cleanup of docs and workspaces.
|
||||
const noCleanup = Boolean(process.env.NO_CLEANUP);
|
||||
|
||||
/**
|
||||
* Use from a test suite to get an object with convenient methods for creating ActiveDocs:
|
||||
*
|
||||
* createDoc(docName): creates a new empty document.
|
||||
* loadFixtureDoc(docName): loads a copy of a fixture document.
|
||||
* loadDoc(docName): loads a given document, e.g. previously created with createDoc().
|
||||
* createFakeSession(): creates a fake DocSession for use when applying user actions.
|
||||
*
|
||||
* Also available are accessors for the created "managers":
|
||||
* getDocManager()
|
||||
* getStorageManager()
|
||||
* getPluginManager()
|
||||
*
|
||||
* It also takes care of cleaning up any created ActiveDocs.
|
||||
* @param persistAcrossCases Don't shut down created ActiveDocs between test cases.
|
||||
* @param useFixturePlugins Use the plugins in `test/fixtures/plugins`
|
||||
*/
|
||||
export function createDocTools(options: {persistAcrossCases?: boolean,
|
||||
useFixturePlugins?: boolean,
|
||||
storageManager?: IDocStorageManager,
|
||||
server?: GristServer} = {}) {
|
||||
let tmpDir: string;
|
||||
let docManager: DocManager;
|
||||
|
||||
async function doBefore() {
|
||||
tmpDir = await createTmpDir();
|
||||
const pluginManager = options.useFixturePlugins ? await createFixturePluginManager() : undefined;
|
||||
docManager = await createDocManager({tmpDir, pluginManager, storageManager: options.storageManager,
|
||||
server: options.server});
|
||||
}
|
||||
|
||||
async function doAfter() {
|
||||
// Clean up at the end of the test suite (in addition to the optional per-test cleanup).
|
||||
await testUtils.captureLog('info', () => docManager.shutdownAll());
|
||||
assert.equal(docManager.numOpenDocs(), 0);
|
||||
await globalUploadSet.cleanupAll();
|
||||
|
||||
// Clean up the temp directory.
|
||||
if (!noCleanup) {
|
||||
await fse.remove(tmpDir);
|
||||
}
|
||||
}
|
||||
|
||||
// Allow using outside of mocha
|
||||
if (typeof before !== "undefined") {
|
||||
before(doBefore);
|
||||
after(doAfter);
|
||||
|
||||
// Check after each test case that all ActiveDocs got shut down.
|
||||
afterEach(async function() {
|
||||
if (!options.persistAcrossCases) {
|
||||
await docManager.shutdownAll();
|
||||
assert.equal(docManager.numOpenDocs(), 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const systemSession = makeExceptionalDocSession('system');
|
||||
return {
|
||||
/** create a fake session for use when applying user actions to a document */
|
||||
createFakeSession(): DocSession {
|
||||
return {client: null, authorizer: new DummyAuthorizer('editors', 'doc')} as any as DocSession;
|
||||
},
|
||||
|
||||
/** create a throw-away, empty document for testing purposes */
|
||||
async createDoc(docName: string): Promise<ActiveDoc> {
|
||||
return docManager.createNewEmptyDoc(systemSession, docName);
|
||||
},
|
||||
|
||||
/** load a copy of a fixture document for testing purposes */
|
||||
async loadFixtureDoc(docName: string): Promise<ActiveDoc> {
|
||||
const copiedDocName = await testUtils.useFixtureDoc(docName, docManager.storageManager);
|
||||
return this.loadDoc(copiedDocName);
|
||||
},
|
||||
|
||||
/** load a copy of a local document at an arbitrary path on disk for testing purposes */
|
||||
async loadLocalDoc(srcPath: string): Promise<ActiveDoc> {
|
||||
const copiedDocName = await testUtils.useLocalDoc(srcPath, docManager.storageManager);
|
||||
return this.loadDoc(copiedDocName);
|
||||
},
|
||||
|
||||
/** like `loadFixtureDoc`, but lets you rename the document on disk */
|
||||
async loadFixtureDocAs(docName: string, alias: string): Promise<ActiveDoc> {
|
||||
const copiedDocName = await testUtils.useFixtureDoc(docName, docManager.storageManager, alias);
|
||||
return this.loadDoc(copiedDocName);
|
||||
},
|
||||
|
||||
/** Loads a given document, e.g. previously created with createDoc() */
|
||||
async loadDoc(docName: string): Promise<ActiveDoc> {
|
||||
return docManager.fetchDoc(systemSession, docName);
|
||||
},
|
||||
|
||||
getDocManager() { return docManager; },
|
||||
getStorageManager() { return docManager.storageManager; },
|
||||
getPluginManager() { return docManager.pluginManager; },
|
||||
|
||||
/** Setup that needs to be done before using the tools, typically called by mocha */
|
||||
before() { return doBefore(); },
|
||||
|
||||
/** Teardown that needs to be done after using the tools, typically called by mocha */
|
||||
after() { return doAfter(); },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a DocManager for tests, complete with a PluginManager and DocStorageManager.
|
||||
* @param options.pluginManager The PluginManager to use; defaults to using a real global singleton
|
||||
* that loads built-in modules.
|
||||
*/
|
||||
export async function createDocManager(
|
||||
options: {tmpDir?: string, pluginManager?: PluginManager,
|
||||
storageManager?: IDocStorageManager,
|
||||
server?: GristServer} = {}): Promise<DocManager> {
|
||||
// Set Grist home to a temporary directory, and wipe it out on exit.
|
||||
const tmpDir = options.tmpDir || await createTmpDir();
|
||||
const docStorageManager = options.storageManager || new DocStorageManager(tmpDir);
|
||||
const pluginManager = options.pluginManager || await getGlobalPluginManager();
|
||||
const store = getDocWorkerMap();
|
||||
const internalPermitStore = store.getPermitStore('1');
|
||||
const externalPermitStore = store.getPermitStore('2');
|
||||
return new DocManager(docStorageManager, pluginManager, null, options.server || {
|
||||
...createDummyGristServer(),
|
||||
getPermitStore() { return internalPermitStore; },
|
||||
getExternalPermitStore() { return externalPermitStore; },
|
||||
getStorageManager() { return docStorageManager; },
|
||||
});
|
||||
}
|
||||
|
||||
export function createDummyGristServer(): GristServer {
|
||||
return {
|
||||
create,
|
||||
getHost() { return 'localhost:4242'; },
|
||||
getHomeUrl() { return 'http://localhost:4242'; },
|
||||
getHomeUrlByDocId() { return Promise.resolve('http://localhost:4242'); },
|
||||
getMergedOrgUrl() { return 'http://localhost:4242'; },
|
||||
getOwnUrl() { return 'http://localhost:4242'; },
|
||||
getPermitStore() { throw new Error('no permit store'); },
|
||||
getExternalPermitStore() { throw new Error('no external permit store'); },
|
||||
getGristConfig() { return { homeUrl: '', timestampMs: 0 }; },
|
||||
getOrgUrl() { return Promise.resolve(''); },
|
||||
getResourceUrl() { return Promise.resolve(''); },
|
||||
getSessions() { throw new Error('no sessions'); },
|
||||
getComm() { throw new Error('no comms'); },
|
||||
getHosts() { throw new Error('no hosts'); },
|
||||
getHomeDBManager() { throw new Error('no db'); },
|
||||
getStorageManager() { throw new Error('no storage manager'); },
|
||||
getNotifier() { throw new Error('no notifier'); },
|
||||
getDocTemplate() { throw new Error('no doc template'); },
|
||||
getTag() { return 'tag'; },
|
||||
sendAppPage() { return Promise.resolve(); },
|
||||
};
|
||||
}
|
||||
|
||||
export async function createTmpDir(): Promise<string> {
|
||||
const tmpRootDir = process.env.TESTDIR || tmpdir();
|
||||
await fse.mkdirs(tmpRootDir);
|
||||
return fse.realpath(await tmp.dirAsync({
|
||||
dir: tmpRootDir,
|
||||
prefix: 'grist_test_',
|
||||
unsafeCleanup: true,
|
||||
keep: noCleanup,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a file with the given name (and simple dummy content) in dirPath, and returns
|
||||
* FileUploadInfo for it.
|
||||
*/
|
||||
export async function createFile(dirPath: string, name: string): Promise<FileUploadInfo> {
|
||||
const absPath = path.join(dirPath, name);
|
||||
await fse.outputFile(absPath, `${name}:${name}\n`);
|
||||
return {
|
||||
absPath,
|
||||
origName: name,
|
||||
size: (await fse.stat(absPath)).size,
|
||||
ext: path.extname(name),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an upload with the given filenames (containg simple dummy content), in the
|
||||
* globalUploadSet, and returns its uploadId. The upload is registered with the given accessId
|
||||
* (userId), and the same id must be used to retrieve it.
|
||||
*/
|
||||
export async function createUpload(fileNames: string[], accessId: string|null): Promise<number> {
|
||||
const {tmpDir, cleanupCallback} = await createTmpUploadDir({});
|
||||
const files = await Promise.all(fileNames.map((name) => createFile(tmpDir, name)));
|
||||
return globalUploadSet.registerUpload(files, tmpDir, cleanupCallback, accessId);
|
||||
}
|
||||
|
||||
|
||||
let _globalPluginManager: PluginManager|null = null;
|
||||
|
||||
// Helper to create a singleton PluginManager. This includes loading built-in plugins. Since most
|
||||
// tests don't make any use of it, it's fine to reuse a single one. For tests that need a custom
|
||||
// one, pass one into createDocManager().
|
||||
export async function getGlobalPluginManager(): Promise<PluginManager> {
|
||||
if (!_globalPluginManager) {
|
||||
const appRoot = getAppRoot();
|
||||
_globalPluginManager = new PluginManager(appRoot);
|
||||
await _globalPluginManager.initialize();
|
||||
}
|
||||
return _globalPluginManager;
|
||||
}
|
||||
|
||||
// Path to the folder where builtIn plugins leave in test/fixtures
|
||||
export const builtInFolder = path.join(testUtils.fixturesRoot, 'plugins/builtInPlugins');
|
||||
|
||||
// Path to the folder where installed plugins leave in test/fixtures
|
||||
export const installedFolder = path.join(testUtils.fixturesRoot, 'plugins/installedPlugins');
|
||||
|
||||
// Creates a plugin manager which loads the plugins in `test/fixtures/plugins`
|
||||
async function createFixturePluginManager() {
|
||||
const p = new PluginManager(builtInFolder, installedFolder);
|
||||
p.appRoot = getAppRoot();
|
||||
await p.initialize();
|
||||
return p;
|
||||
}
|
||||
167
test/server/gristClient.ts
Normal file
167
test/server/gristClient.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import { DocAction } from 'app/common/DocActions';
|
||||
import { FlexServer } from 'app/server/lib/FlexServer';
|
||||
import axios from 'axios';
|
||||
import pick = require('lodash/pick');
|
||||
import * as WebSocket from 'ws';
|
||||
|
||||
interface GristRequest {
|
||||
reqId: number;
|
||||
method: string;
|
||||
args: any[];
|
||||
}
|
||||
|
||||
interface GristResponse {
|
||||
reqId: number;
|
||||
error?: string;
|
||||
errorCode?: string;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
interface GristMessage {
|
||||
type: 'clientConnect' | 'docUserAction';
|
||||
docFD: number;
|
||||
data: any;
|
||||
}
|
||||
|
||||
export class GristClient {
|
||||
public messages: GristMessage[] = [];
|
||||
|
||||
private _requestId: number = 0;
|
||||
private _pending: Array<GristResponse|GristMessage> = [];
|
||||
private _consumer: () => void;
|
||||
private _ignoreTrivialActions: boolean = false;
|
||||
|
||||
constructor(public ws: any) {
|
||||
ws.onmessage = (data: any) => {
|
||||
const msg = pick(JSON.parse(data.data),
|
||||
['reqId', 'error', 'errorCode', 'data', 'type', 'docFD']);
|
||||
if (this._ignoreTrivialActions && msg.type === 'docUserAction' &&
|
||||
msg.data?.actionGroup?.internal === true &&
|
||||
msg.data?.docActions?.length === 0) {
|
||||
return;
|
||||
}
|
||||
this._pending.push(msg);
|
||||
if (this._consumer) { this._consumer(); }
|
||||
};
|
||||
}
|
||||
|
||||
// After a document is opened, the sandbox recomputes its formulas and sends any changes.
|
||||
// The client will receive an update even if there are no changes. This may be useful in
|
||||
// the future to know that the document is up to date. But for testing, this asynchronous
|
||||
// message can be awkward. Call this method to ignore it.
|
||||
public ignoreTrivialActions() {
|
||||
this._ignoreTrivialActions = true;
|
||||
}
|
||||
|
||||
public flush() {
|
||||
this._pending = [];
|
||||
}
|
||||
|
||||
public shift() {
|
||||
return this._pending.shift();
|
||||
}
|
||||
|
||||
public count() {
|
||||
return this._pending.length;
|
||||
}
|
||||
|
||||
public async read(): Promise<any> {
|
||||
for (;;) {
|
||||
if (this._pending.length) {
|
||||
return this._pending.shift();
|
||||
}
|
||||
await new Promise(resolve => this._consumer = resolve);
|
||||
}
|
||||
}
|
||||
|
||||
public async readMessage(): Promise<GristMessage> {
|
||||
const result = await this.read();
|
||||
if (!result.type) {
|
||||
throw new Error(`message looks wrong: ${JSON.stringify(result)}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public async readResponse(): Promise<GristResponse> {
|
||||
this.messages = [];
|
||||
for (;;) {
|
||||
const result = await this.read();
|
||||
if (result.reqId === undefined) {
|
||||
this.messages.push(result);
|
||||
continue;
|
||||
}
|
||||
if (result.reqId !== this._requestId) {
|
||||
throw new Error("unexpected request id");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to read the next docUserAction ignoring anything else (e.g. a duplicate clientConnect).
|
||||
public async readDocUserAction(): Promise<DocAction[]> {
|
||||
while (true) { // eslint-disable-line no-constant-condition
|
||||
const msg = await this.readMessage();
|
||||
if (msg.type === 'docUserAction') {
|
||||
return msg.data.docActions;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async send(method: string, ...args: any[]): Promise<GristResponse> {
|
||||
const p = this.readResponse();
|
||||
this._requestId++;
|
||||
const req: GristRequest = {
|
||||
reqId: this._requestId,
|
||||
method,
|
||||
args
|
||||
};
|
||||
this.ws.send(JSON.stringify(req));
|
||||
const result = await p;
|
||||
return result;
|
||||
}
|
||||
|
||||
public async close() {
|
||||
this.ws.terminate();
|
||||
this.ws.close();
|
||||
}
|
||||
|
||||
public async openDocOnConnect(docId: string) {
|
||||
const msg = await this.readMessage();
|
||||
if (msg.type !== 'clientConnect') { throw new Error('expected clientConnect'); }
|
||||
const openDoc = await this.send('openDoc', docId);
|
||||
if (openDoc.error) { throw new Error('error in openDocOnConnect'); }
|
||||
return openDoc;
|
||||
}
|
||||
}
|
||||
|
||||
export async function openClient(server: FlexServer, email: string, org: string,
|
||||
emailHeader?: string): Promise<GristClient> {
|
||||
const headers: Record<string, string> = {};
|
||||
if (!emailHeader) {
|
||||
const resp = await axios.get(`${server.getOwnUrl()}/test/session`);
|
||||
const cookie = resp.headers['set-cookie'][0];
|
||||
if (email !== 'anon@getgrist.com') {
|
||||
const cid = decodeURIComponent(cookie.split('=')[1].split(';')[0]);
|
||||
const comm = server.getComm();
|
||||
const sessionId = comm.getSessionIdFromCookie(cid);
|
||||
const scopedSession = comm.getOrCreateSession(sessionId, {org});
|
||||
const profile = { email, email_verified: true, name: "Someone" };
|
||||
await scopedSession.updateUserProfile({} as any, profile);
|
||||
}
|
||||
headers.Cookie = cookie;
|
||||
} else {
|
||||
headers[emailHeader] = email;
|
||||
}
|
||||
const ws = new WebSocket('ws://localhost:' + server.getOwnPort() + `/o/${org}`, {
|
||||
headers
|
||||
});
|
||||
await new Promise(function(resolve, reject) {
|
||||
ws.on('open', function() {
|
||||
resolve(ws);
|
||||
});
|
||||
ws.on('error', function(err: any) {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
return new GristClient(ws);
|
||||
}
|
||||
305
test/server/lib/Authorizer.ts
Normal file
305
test/server/lib/Authorizer.ts
Normal file
@@ -0,0 +1,305 @@
|
||||
import {parseUrlId} from 'app/common/gristUrls';
|
||||
import {HomeDBManager} from 'app/gen-server/lib/HomeDBManager';
|
||||
import {DocManager} from 'app/server/lib/DocManager';
|
||||
import {FlexServer} from 'app/server/lib/FlexServer';
|
||||
import axios from 'axios';
|
||||
import {assert} from 'chai';
|
||||
import {toPairs} from 'lodash';
|
||||
import {createInitialDb, removeConnection, setUpDB} from 'test/gen-server/seed';
|
||||
import {configForUser, getGristConfig} from 'test/gen-server/testUtils';
|
||||
import {createDocTools} from 'test/server/docTools';
|
||||
import {openClient} from 'test/server/gristClient';
|
||||
import * as testUtils from 'test/server/testUtils';
|
||||
import * as uuidv4 from 'uuid/v4';
|
||||
|
||||
let serverUrl: string;
|
||||
let server: FlexServer;
|
||||
let dbManager: HomeDBManager;
|
||||
|
||||
async function activateServer(home: FlexServer, docManager: DocManager) {
|
||||
await home.initHomeDBManager();
|
||||
home.addHosts();
|
||||
home.addDocWorkerMap();
|
||||
home.addAccessMiddleware();
|
||||
dbManager = home.getHomeDBManager();
|
||||
await home.loadConfig({});
|
||||
home.addSessions();
|
||||
home.addHealthCheck();
|
||||
docManager.testSetHomeDbManager(dbManager);
|
||||
home.testSetDocManager(docManager);
|
||||
await home.start();
|
||||
home.addAccessMiddleware();
|
||||
home.addApiMiddleware();
|
||||
home.addJsonSupport();
|
||||
await home.addLandingPages();
|
||||
home.addHomeApi();
|
||||
await home.addDoc();
|
||||
home.addApiErrorHandlers();
|
||||
serverUrl = home.getOwnUrl();
|
||||
}
|
||||
|
||||
const chimpy = configForUser('Chimpy');
|
||||
const charon = configForUser('Charon');
|
||||
|
||||
const fixtures: {[docName: string]: string|null} = {
|
||||
Bananas: 'Hello.grist',
|
||||
Pluto: 'Hello.grist',
|
||||
};
|
||||
|
||||
describe('Authorizer', function() {
|
||||
|
||||
testUtils.setTmpLogLevel('fatal');
|
||||
|
||||
server = new FlexServer(0, 'test docWorker');
|
||||
const docTools = createDocTools({persistAcrossCases: true, useFixturePlugins: false,
|
||||
server});
|
||||
const docs: {[name: string]: {id: string}} = {};
|
||||
|
||||
// Loads the fixtures documents so that they are available to the doc worker under the correct
|
||||
// names.
|
||||
async function loadFixtureDocs() {
|
||||
for (const [docName, fixtureDoc] of toPairs(fixtures)) {
|
||||
const docId = String(await dbManager.testGetId(docName));
|
||||
if (fixtureDoc) {
|
||||
await docTools.loadFixtureDocAs(fixtureDoc, docId);
|
||||
} else {
|
||||
await docTools.createDoc(docId);
|
||||
}
|
||||
docs[docName] = {id: docId};
|
||||
}
|
||||
}
|
||||
|
||||
let oldEnv: testUtils.EnvironmentSnapshot;
|
||||
before(async function() {
|
||||
this.timeout(5000);
|
||||
setUpDB(this);
|
||||
oldEnv = new testUtils.EnvironmentSnapshot();
|
||||
process.env.GRIST_PROXY_AUTH_HEADER = 'X-email';
|
||||
await createInitialDb();
|
||||
await activateServer(server, docTools.getDocManager());
|
||||
await loadFixtureDocs();
|
||||
});
|
||||
|
||||
after(async function() {
|
||||
const messages = await testUtils.captureLog('warn', async () => {
|
||||
await server.close();
|
||||
await removeConnection();
|
||||
});
|
||||
assert.lengthOf(messages, 0);
|
||||
oldEnv.restore();
|
||||
});
|
||||
|
||||
// TODO XXX Is it safe to remove this support now?
|
||||
// (It used to be implemented in getDocAccessInfo() in Authorizer.ts).
|
||||
it.skip("viewer gets redirect by title", async function() {
|
||||
const resp = await axios.get(`${serverUrl}/o/pr/doc/Bananas`, chimpy);
|
||||
assert.equal(resp.status, 200);
|
||||
assert.equal(getGristConfig(resp.data).assignmentId, 'sample_6');
|
||||
assert.match(resp.request.res.responseUrl, /\/doc\/sample_6$/);
|
||||
const resp2 = await axios.get(`${serverUrl}/o/nasa/doc/Pluto`, chimpy);
|
||||
assert.equal(resp2.status, 200);
|
||||
assert.equal(getGristConfig(resp2.data).assignmentId, 'sample_2');
|
||||
assert.match(resp2.request.res.responseUrl, /\/doc\/sample_2$/);
|
||||
});
|
||||
|
||||
it("stranger gets consistent refusal regardless of title", async function() {
|
||||
const resp = await axios.get(`${serverUrl}/o/pr/doc/Bananas`, charon);
|
||||
assert.equal(resp.status, 404);
|
||||
assert.notMatch(resp.data, /sample_6/);
|
||||
const resp2 = await axios.get(`${serverUrl}/o/pr/doc/Bananas2`, charon);
|
||||
assert.equal(resp2.status, 404);
|
||||
assert.notMatch(resp.data, /sample_6/);
|
||||
assert.deepEqual(resp.data, resp2.data);
|
||||
});
|
||||
|
||||
it("viewer can access title", async function() {
|
||||
const resp = await axios.get(`${serverUrl}/o/pr/doc/sample_6`, chimpy);
|
||||
assert.equal(resp.status, 200);
|
||||
const config = getGristConfig(resp.data);
|
||||
assert.equal(config.getDoc![config.assignmentId!].name, 'Bananas');
|
||||
});
|
||||
|
||||
it("stranger cannot access title", async function() {
|
||||
const resp = await axios.get(`${serverUrl}/o/pr/doc/sample_6`, charon);
|
||||
assert.equal(resp.status, 403);
|
||||
assert.notMatch(resp.data, /Bananas/);
|
||||
});
|
||||
|
||||
it("viewer cannot access document from wrong org", async function() {
|
||||
const resp = await axios.get(`${serverUrl}/o/nasa/doc/sample_6`, chimpy);
|
||||
assert.equal(resp.status, 404);
|
||||
});
|
||||
|
||||
it("websocket allows openDoc for viewer", async function() {
|
||||
const cli = await openClient(server, 'chimpy@getgrist.com', 'pr');
|
||||
cli.ignoreTrivialActions();
|
||||
assert.equal((await cli.readMessage()).type, 'clientConnect');
|
||||
const openDoc = await cli.send("openDoc", "sample_6");
|
||||
assert.equal(openDoc.error, undefined);
|
||||
assert.match(JSON.stringify(openDoc.data), /Table1/);
|
||||
await cli.close();
|
||||
});
|
||||
|
||||
it("websocket forbids openDoc for stranger", async function() {
|
||||
const cli = await openClient(server, 'charon@getgrist.com', 'pr');
|
||||
cli.ignoreTrivialActions();
|
||||
assert.equal((await cli.readMessage()).type, 'clientConnect');
|
||||
const openDoc = await cli.send("openDoc", "sample_6");
|
||||
assert.match(openDoc.error!, /No view access/);
|
||||
assert.equal(openDoc.data, undefined);
|
||||
assert.match(openDoc.errorCode!, /AUTH_NO_VIEW/);
|
||||
await cli.close();
|
||||
});
|
||||
|
||||
it("websocket forbids applyUserActions for viewer", async function() {
|
||||
const cli = await openClient(server, 'charon@getgrist.com', 'nasa');
|
||||
cli.ignoreTrivialActions();
|
||||
assert.equal((await cli.readMessage()).type, 'clientConnect');
|
||||
const openDoc = await cli.openDocOnConnect("sample_2");
|
||||
assert.equal(openDoc.error, undefined);
|
||||
const nonce = uuidv4();
|
||||
const applyUserActions = await cli.send("applyUserActions",
|
||||
0,
|
||||
[["UpdateRecord", "Table1", 1, {A: nonce}], {}]);
|
||||
assert.lengthOf(cli.messages, 0); // no user actions pushed to client
|
||||
assert.match(applyUserActions.error!, /No write access/);
|
||||
assert.match(applyUserActions.errorCode!, /AUTH_NO_EDIT/);
|
||||
const fetchTable = await cli.send("fetchTable", 0, "Table1");
|
||||
assert.equal(fetchTable.error, undefined);
|
||||
assert.notInclude(JSON.stringify(fetchTable.data), nonce);
|
||||
await cli.close();
|
||||
});
|
||||
|
||||
it("websocket allows applyUserActions for editor", async function() {
|
||||
const cli = await openClient(server, 'chimpy@getgrist.com', 'nasa');
|
||||
cli.ignoreTrivialActions();
|
||||
assert.equal((await cli.readMessage()).type, 'clientConnect');
|
||||
const openDoc = await cli.openDocOnConnect("sample_2");
|
||||
assert.equal(openDoc.error, undefined);
|
||||
const nonce = uuidv4();
|
||||
const applyUserActions = await cli.send("applyUserActions",
|
||||
0,
|
||||
[["UpdateRecord", "Table1", 1, {A: nonce}]]);
|
||||
assert.lengthOf(cli.messages, 1); // user actions pushed to client
|
||||
assert.equal(applyUserActions.error, undefined);
|
||||
const fetchTable = await cli.send("fetchTable", 0, "Table1");
|
||||
assert.equal(fetchTable.error, undefined);
|
||||
assert.include(JSON.stringify(fetchTable.data), nonce);
|
||||
await cli.close();
|
||||
});
|
||||
|
||||
it("can keep different simultaneous clients of a doc straight", async function() {
|
||||
const editor = await openClient(server, 'chimpy@getgrist.com', 'nasa');
|
||||
assert.equal((await editor.readMessage()).type, 'clientConnect');
|
||||
const viewer = await openClient(server, 'charon@getgrist.com', 'nasa');
|
||||
assert.equal((await viewer.readMessage()).type, 'clientConnect');
|
||||
const stranger = await openClient(server, 'kiwi@getgrist.com', 'nasa');
|
||||
assert.equal((await stranger.readMessage()).type, 'clientConnect');
|
||||
|
||||
editor.ignoreTrivialActions();
|
||||
viewer.ignoreTrivialActions();
|
||||
stranger.ignoreTrivialActions();
|
||||
assert.equal((await editor.send("openDoc", "sample_2")).error, undefined);
|
||||
assert.equal((await viewer.send("openDoc", "sample_2")).error, undefined);
|
||||
assert.match((await stranger.send("openDoc", "sample_2")).error!, /No view access/);
|
||||
|
||||
const action = [0, [["UpdateRecord", "Table1", 1, {A: "foo"}]]];
|
||||
assert.equal((await editor.send("applyUserActions", ...action)).error, undefined);
|
||||
assert.match((await viewer.send("applyUserActions", ...action)).error!, /No write access/);
|
||||
// Different message here because sending actions without a doc being open.
|
||||
assert.match((await stranger.send("applyUserActions", ...action)).error!, /Invalid/);
|
||||
});
|
||||
|
||||
it("previewer has view access to docs", async function() {
|
||||
const cli = await openClient(server, 'thumbnail@getgrist.com', 'nasa');
|
||||
cli.ignoreTrivialActions();
|
||||
assert.equal((await cli.readMessage()).type, 'clientConnect');
|
||||
const openDoc = await cli.send("openDoc", "sample_2");
|
||||
assert.equal(openDoc.error, undefined);
|
||||
const nonce = uuidv4();
|
||||
const applyUserActions = await cli.send("applyUserActions",
|
||||
0,
|
||||
[["UpdateRecord", "Table1", 1, {A: nonce}], {}]);
|
||||
assert.lengthOf(cli.messages, 0); // no user actions pushed to client
|
||||
assert.match(applyUserActions.error!, /No write access/);
|
||||
assert.match(applyUserActions.errorCode!, /AUTH_NO_EDIT/);
|
||||
const fetchTable = await cli.send("fetchTable", 0, "Table1");
|
||||
assert.equal(fetchTable.error, undefined);
|
||||
assert.notInclude(JSON.stringify(fetchTable.data), nonce);
|
||||
await cli.close();
|
||||
});
|
||||
|
||||
it("viewer can fork doc", async function() {
|
||||
const cli = await openClient(server, 'charon@getgrist.com', 'nasa');
|
||||
cli.ignoreTrivialActions();
|
||||
assert.equal((await cli.readMessage()).type, 'clientConnect');
|
||||
const openDoc = await cli.send("openDoc", "sample_2");
|
||||
assert.equal(openDoc.error, undefined);
|
||||
const result = await cli.send("fork", 0);
|
||||
assert.equal(result.data.docId, result.data.urlId);
|
||||
const parts = parseUrlId(result.data.docId);
|
||||
assert.equal(parts.trunkId, "sample_2");
|
||||
assert.isAbove(parts.forkId!.length, 4);
|
||||
assert.equal(parts.forkUserId, await dbManager.testGetId('Charon') as number);
|
||||
});
|
||||
|
||||
it("anon can fork doc", async function() {
|
||||
// anon does not have access to doc initially
|
||||
const cli = await openClient(server, 'anon@getgrist.com', 'nasa');
|
||||
cli.ignoreTrivialActions();
|
||||
assert.equal((await cli.readMessage()).type, 'clientConnect');
|
||||
let openDoc = await cli.send("openDoc", "sample_2");
|
||||
assert.match(openDoc.error!, /No view access/);
|
||||
|
||||
// grant anon access to doc and retry
|
||||
await dbManager.updateDocPermissions({
|
||||
userId: await dbManager.testGetId('Chimpy') as number,
|
||||
urlId: 'sample_2',
|
||||
org: 'nasa'
|
||||
}, {users: {"anon@getgrist.com": "viewers"}});
|
||||
dbManager.flushDocAuthCache();
|
||||
openDoc = await cli.send("openDoc", "sample_2");
|
||||
assert.equal(openDoc.error, undefined);
|
||||
|
||||
// make a fork
|
||||
const result = await cli.send("fork", 0);
|
||||
assert.equal(result.data.docId, result.data.urlId);
|
||||
const parts = parseUrlId(result.data.docId);
|
||||
assert.equal(parts.trunkId, "sample_2");
|
||||
assert.isAbove(parts.forkId!.length, 4);
|
||||
assert.equal(parts.forkUserId, undefined);
|
||||
});
|
||||
|
||||
it("can set user via GRIST_PROXY_AUTH_HEADER", async function() {
|
||||
// User can access a doc by setting header.
|
||||
const docUrl = `${serverUrl}/o/pr/api/docs/sample_6`;
|
||||
const resp = await axios.get(docUrl, {
|
||||
headers: {'X-email': 'chimpy@getgrist.com'}
|
||||
});
|
||||
assert.equal(resp.data.name, 'Bananas');
|
||||
|
||||
// Unknown user is denied.
|
||||
await assert.isRejected(axios.get(docUrl, {
|
||||
headers: {'X-email': 'notchimpy@getgrist.com'}
|
||||
}));
|
||||
|
||||
// User can access a doc via websocket by setting header.
|
||||
let cli = await openClient(server, 'chimpy@getgrist.com', 'pr', 'X-email');
|
||||
cli.ignoreTrivialActions();
|
||||
assert.equal((await cli.readMessage()).type, 'clientConnect');
|
||||
let openDoc = await cli.send("openDoc", "sample_6");
|
||||
assert.equal(openDoc.error, undefined);
|
||||
assert.match(JSON.stringify(openDoc.data), /Table1/);
|
||||
await cli.close();
|
||||
|
||||
// Unknown user is denied.
|
||||
cli = await openClient(server, 'notchimpy@getgrist.com', 'pr', 'X-email');
|
||||
cli.ignoreTrivialActions();
|
||||
assert.equal((await cli.readMessage()).type, 'clientConnect');
|
||||
openDoc = await cli.send("openDoc", "sample_6");
|
||||
assert.match(openDoc.error!, /No view access/);
|
||||
assert.equal(openDoc.data, undefined);
|
||||
assert.match(openDoc.errorCode!, /AUTH_NO_VIEW/);
|
||||
await cli.close();
|
||||
});
|
||||
});
|
||||
2589
test/server/lib/DocApi.ts
Normal file
2589
test/server/lib/DocApi.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -169,7 +169,7 @@ export function assertMatchArray(stringArray: string[], regexArray: RegExp[]) {
|
||||
* @param {String} errCode - Error code to check against `err.code` from the caller.
|
||||
* @param {RegExp} errRegexp - Regular expression to check against `err.message` from the caller.
|
||||
*/
|
||||
export function expectRejection(promise: Promise<any>, errCode: number, errRegexp: RegExp) {
|
||||
export function expectRejection(promise: Promise<any>, errCode: number|string, errRegexp: RegExp) {
|
||||
return promise
|
||||
.then(function() {
|
||||
assert(false, "Expected promise to return an error: " + errCode);
|
||||
@@ -307,4 +307,11 @@ export class EnvironmentSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getBuildFile(relativePath: string): Promise<string> {
|
||||
if (await fse.pathExists(path.join('_build', relativePath))) {
|
||||
return path.join('_build', relativePath);
|
||||
}
|
||||
return path.join('_build', 'core', relativePath);
|
||||
}
|
||||
|
||||
export { assert };
|
||||
|
||||
Reference in New Issue
Block a user