2022-08-18 21:08:39 +00:00
|
|
|
import { ClientScope } from 'app/client/components/ClientScope';
|
|
|
|
import { Disposable } from 'app/client/lib/dispose';
|
|
|
|
import { ClientProcess, SafeBrowser } from 'app/client/lib/SafeBrowser';
|
|
|
|
import { LocalPlugin } from 'app/common/plugin';
|
|
|
|
import { PluginInstance } from 'app/common/PluginInstance';
|
|
|
|
import { GristAPI, RPC_GRISTAPI_INTERFACE } from 'app/plugin/GristAPI';
|
|
|
|
import { Storage } from 'app/plugin/StorageAPI';
|
|
|
|
import { checkers } from 'app/plugin/TypeCheckers';
|
|
|
|
import { assert } from 'chai';
|
|
|
|
import { Rpc } from 'grain-rpc';
|
|
|
|
import { noop } from 'lodash';
|
|
|
|
import { basename } from 'path';
|
|
|
|
import * as sinon from 'sinon';
|
|
|
|
import * as clientUtil from 'test/client/clientUtil';
|
|
|
|
import * as tic from "ts-interface-checker";
|
|
|
|
import { createCheckers } from "ts-interface-checker";
|
|
|
|
import * as url from 'url';
|
|
|
|
|
|
|
|
clientUtil.setTmpMochaGlobals();
|
|
|
|
|
|
|
|
const LOG_RPC = false; // tslint:disable-line:prefer-const
|
|
|
|
|
|
|
|
// uncomment next line to turn on rpc logging
|
|
|
|
// LOG_RPC = true;
|
|
|
|
|
|
|
|
describe('SafeBrowser', function() {
|
|
|
|
|
|
|
|
let clientScope: any;
|
|
|
|
const sandbox = sinon.createSandbox();
|
|
|
|
let browserProcesses: Array<{path: string, proc: ClientProcess}> = [];
|
|
|
|
|
|
|
|
let disposeSpy: sinon.SinonSpy;
|
|
|
|
const cleanup: Array<() => void> = [];
|
|
|
|
|
|
|
|
beforeEach(function() {
|
|
|
|
const callPluginFunction = sinon.stub();
|
|
|
|
callPluginFunction
|
|
|
|
.withArgs('testing-plugin', 'unsafeNode', 'func1')
|
|
|
|
.callsFake( (...args) => 'From Russia ' + args[3][0] + "!");
|
|
|
|
callPluginFunction
|
|
|
|
.withArgs('testing-plugin', 'unsafeNode', 'funkyName')
|
|
|
|
.throws();
|
|
|
|
clientScope = new ClientScope();
|
|
|
|
|
|
|
|
browserProcesses = [];
|
|
|
|
sandbox.stub(SafeBrowser, 'createWorker').callsFake(createProcess);
|
|
|
|
sandbox.stub(SafeBrowser, 'createView').callsFake(createProcess);
|
|
|
|
sandbox.stub(PluginInstance.prototype, 'getRenderTarget').returns(noop);
|
|
|
|
disposeSpy = sandbox.spy(Disposable.prototype, 'dispose');
|
|
|
|
});
|
|
|
|
|
|
|
|
afterEach(function() {
|
|
|
|
sandbox.restore();
|
|
|
|
for (const cb of cleanup) { cb(); }
|
|
|
|
cleanup.splice(0);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should support rpc', async function() {
|
|
|
|
const {safeBrowser, pluginRpc} = createSafeBrowser('test_rpc');
|
|
|
|
const foo = pluginRpc.getStub<Foo>('grist@test_rpc', FooDescription);
|
|
|
|
await safeBrowser.activate();
|
|
|
|
assert.equal(await foo.foo('rpc test'), 'foo rpc test');
|
|
|
|
});
|
|
|
|
|
|
|
|
it("can stub view processes", async function() {
|
|
|
|
const {safeBrowser, pluginRpc} = createSafeBrowser('test_render');
|
|
|
|
const foo = pluginRpc.getStub<Foo>('grist@test_render_view', FooDescription);
|
|
|
|
await safeBrowser.activate();
|
|
|
|
assert.equal(await foo.foo('rpc test'), 'foo rpc test from test_render_view');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('can forward rpc to a view process', async function() {
|
|
|
|
const {safeBrowser, pluginRpc} = createSafeBrowser("test_forward");
|
|
|
|
const foo = pluginRpc.getStub<Foo>('grist@test_forward', FooDescription);
|
|
|
|
await safeBrowser.activate();
|
|
|
|
assert.equal(await foo.foo("safeBrowser"), "foo safeBrowser from test_forward_view");
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should forward messages', async function() {
|
|
|
|
const {safeBrowser, pluginRpc} = createSafeBrowser("test_messages");
|
|
|
|
const foo = pluginRpc.getStub<Foo>('foo@test_messages', FooDescription);
|
|
|
|
await safeBrowser.activate();
|
|
|
|
assert.equal(await foo.foo("safeBrowser"), "from message view");
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should support disposing a rendered view', async function() {
|
|
|
|
const {safeBrowser, pluginRpc} = createSafeBrowser("test_dispose");
|
|
|
|
const foo = pluginRpc.getStub<Foo>('grist@test_dispose', FooDescription);
|
|
|
|
await safeBrowser.activate();
|
|
|
|
await foo.foo("safeBrowser");
|
|
|
|
assert.deepEqual(browserProcesses.map(p => p.path), ["test_dispose", "test_dispose_view1", "test_dispose_view2"]);
|
|
|
|
|
|
|
|
assert.equal(disposeSpy.calledOn(processByName("test_dispose_view1")!), true);
|
|
|
|
assert.equal(disposeSpy.calledOn(processByName("test_dispose_view2")!), false);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should dispose each process on deactivation', async function() {
|
|
|
|
const {safeBrowser, pluginRpc} = createSafeBrowser("test_dispose");
|
|
|
|
const foo = pluginRpc.getStub<Foo>('grist@test_dispose', FooDescription);
|
|
|
|
await safeBrowser.activate();
|
|
|
|
await foo.foo("safeBrowser");
|
|
|
|
await safeBrowser.deactivate();
|
|
|
|
assert.deepEqual(browserProcesses.map(p => p.path), ["test_dispose", "test_dispose_view1", "test_dispose_view2"]);
|
|
|
|
for (const {proc} of browserProcesses) {
|
|
|
|
assert.equal(disposeSpy.calledOn(proc), true);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// it('should allow calling unsafeNode functions', async function() {
|
|
|
|
// const {safeBrowser, pluginRpc} = createSafeBrowser("test_function_call");
|
|
|
|
// const rpc = (safeBrowser as any)._pluginInstance.rpc as Rpc;
|
|
|
|
// const foo = rpc.getStub<Foo>('grist@test_function_call', FooDescription);
|
|
|
|
// await safeBrowser.activate();
|
|
|
|
// assert.equal(await foo.foo('func1'), 'From Russia with love!');
|
|
|
|
// await assert.isRejected(foo.foo('funkyName'));
|
|
|
|
// });
|
|
|
|
|
|
|
|
it('should allow access to client scope interfaces', async function() {
|
|
|
|
const {safeBrowser, pluginRpc} = createSafeBrowser("test_client_scope");
|
|
|
|
const foo = pluginRpc.getStub<Foo>('grist@test_client_scope', FooDescription);
|
|
|
|
await safeBrowser.activate();
|
|
|
|
assert.equal(await foo.foo('green'), '#0f0');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should allow access to client scope interfaces from view', async function() {
|
|
|
|
const {safeBrowser, pluginRpc} = createSafeBrowser("test_client_scope_from_view");
|
|
|
|
const foo = pluginRpc.getStub<Foo>('grist@test_client_scope_from_view', FooDescription);
|
|
|
|
await safeBrowser.activate();
|
|
|
|
assert.equal(await foo.foo('red'), 'red#f00');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should have type-safe access to client scope interfaces', async function() {
|
|
|
|
const {safeBrowser, pluginRpc} = createSafeBrowser("test_client_scope_typed");
|
|
|
|
const foo = pluginRpc.getStub<Foo>('grist@test_client_scope_typed', FooDescription);
|
|
|
|
await safeBrowser.activate();
|
|
|
|
await assert.isRejected(foo.foo('test'), /is not a string/);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should allow creating a view process from grist', async function() {
|
|
|
|
const {safeBrowser, pluginRpc} = createSafeBrowser("test_view_process");
|
|
|
|
// let's call buildDom on test_rpc
|
|
|
|
const proc = safeBrowser.createViewProcess("test_rpc");
|
|
|
|
|
|
|
|
// rpc should work
|
|
|
|
const foo = pluginRpc.getStub<Foo>('grist@test_rpc', FooDescription);
|
|
|
|
assert.equal(await foo.foo('Santa'), 'foo Santa');
|
|
|
|
|
|
|
|
// now let's dispose
|
|
|
|
proc.dispose();
|
|
|
|
});
|
|
|
|
|
|
|
|
function createProcess(safeBrowser: SafeBrowser, _rpc: Rpc, src: string) {
|
|
|
|
const path: string = basename(url.parse(src).pathname!);
|
|
|
|
const rpc = new Rpc({logger: LOG_RPC ? {
|
|
|
|
// let's prepend path to the console 'info' and 'warn' channels
|
|
|
|
info: console.info.bind(console, path), // tslint:disable-line:no-console
|
|
|
|
warn: console.warn.bind(console, path), // tslint:disable-line:no-console
|
|
|
|
} : {}, sendMessage: _rpc.receiveMessage.bind(_rpc)});
|
|
|
|
_rpc.setSendMessage(msg => rpc.receiveMessage(msg));
|
|
|
|
const api = rpc.getStub<GristAPI>(RPC_GRISTAPI_INTERFACE, checkers.GristAPI);
|
|
|
|
function ready() {
|
|
|
|
rpc.processIncoming();
|
|
|
|
void rpc.sendReadyMessage();
|
|
|
|
}
|
|
|
|
// Start up the mock process for the plugin.
|
|
|
|
const proc = new ClientProcess(safeBrowser, _rpc);
|
|
|
|
PROCESSES[path]({rpc, api, ready });
|
|
|
|
browserProcesses.push({path, proc});
|
|
|
|
return proc;
|
|
|
|
}
|
|
|
|
|
|
|
|
// At the moment, only the .definition field matters for SafeBrowser.
|
|
|
|
const localPlugin: LocalPlugin = {
|
|
|
|
manifest: {
|
|
|
|
components: { safeBrowser: 'main' },
|
|
|
|
contributions: {}
|
|
|
|
},
|
|
|
|
id: "testing-plugin",
|
|
|
|
path: ""
|
|
|
|
};
|
|
|
|
function createSafeBrowser(mainPath: string): {safeBrowser: SafeBrowser, pluginRpc: Rpc} {
|
|
|
|
const pluginInstance = new PluginInstance(localPlugin, {});
|
2023-09-19 18:44:22 +00:00
|
|
|
const safeBrowser = new SafeBrowser({
|
|
|
|
pluginInstance,
|
|
|
|
clientScope,
|
|
|
|
untrustedContentOrigin: '',
|
|
|
|
mainPath,
|
|
|
|
baseLogger: {},
|
|
|
|
});
|
2022-08-18 21:08:39 +00:00
|
|
|
cleanup.push(() => safeBrowser.deactivate());
|
|
|
|
pluginInstance.rpc.registerForwarder(mainPath, safeBrowser);
|
|
|
|
return {safeBrowser, pluginRpc: pluginInstance.rpc};
|
|
|
|
}
|
|
|
|
|
|
|
|
function processByName(name: string): ClientProcess|undefined {
|
|
|
|
const procInfo = browserProcesses.find(p => (p.path === name));
|
|
|
|
return procInfo ? procInfo.proc : undefined;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A Dummy Api to contribute to.
|
|
|
|
*/
|
|
|
|
interface Foo {
|
|
|
|
foo(name: string): Promise<string>;
|
|
|
|
}
|
|
|
|
|
|
|
|
const FooDescription = createCheckers({
|
|
|
|
Foo: tic.iface([], {
|
|
|
|
foo: tic.func("string", tic.param("name", "string")),
|
|
|
|
})
|
|
|
|
}).Foo;
|
|
|
|
|
|
|
|
interface TestProcesses {
|
|
|
|
[s: string]: (grist: GristModule) => void;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This interface describes what exposes grist-plugin-api.ts to the plugin.
|
|
|
|
*/
|
|
|
|
interface GristModule {
|
|
|
|
rpc: Rpc;
|
|
|
|
api: GristAPI;
|
|
|
|
ready(): void;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The safeBrowser's script needed for test.
|
|
|
|
*/
|
|
|
|
const PROCESSES: TestProcesses = {
|
|
|
|
test_rpc: (grist: GristModule) => {
|
|
|
|
class MyFoo {
|
|
|
|
public async foo(name: string): Promise<string> {
|
|
|
|
return 'foo ' + name;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
grist.rpc.registerImpl<Foo>('grist', new MyFoo(), FooDescription);
|
|
|
|
grist.ready();
|
|
|
|
},
|
|
|
|
async test_render(grist: GristModule) {
|
|
|
|
await grist.api.render('test_render_view', 'fullscreen');
|
|
|
|
grist.ready();
|
|
|
|
},
|
|
|
|
test_render_view(grist: GristModule) {
|
|
|
|
grist.rpc.registerImpl<Foo>('grist', {
|
|
|
|
foo: (name: string) => `foo ${name} from test_render_view`
|
|
|
|
});
|
|
|
|
grist.ready();
|
|
|
|
},
|
|
|
|
async test_forward(grist: GristModule) {
|
|
|
|
grist.rpc.registerImpl<Foo>('grist', {
|
|
|
|
foo: (name: string) => viewFoo.foo(name)
|
|
|
|
});
|
|
|
|
grist.api.render('test_forward_view', 'fullscreen'); // eslint-disable-line @typescript-eslint/no-floating-promises
|
|
|
|
const viewFoo = grist.rpc.getStub<Foo>('foo@test_forward_view', FooDescription);
|
|
|
|
grist.ready();
|
|
|
|
},
|
|
|
|
test_forward_view: (grist: GristModule) => {
|
|
|
|
grist.rpc.registerImpl<Foo>('foo', {
|
|
|
|
foo: async (name) => `foo ${name} from test_forward_view`
|
|
|
|
}, FooDescription);
|
|
|
|
grist.ready();
|
|
|
|
},
|
|
|
|
test_messages: (grist: GristModule) => {
|
|
|
|
grist.rpc.registerImpl<Foo>('foo', {
|
|
|
|
foo(name): Promise<string> {
|
|
|
|
return new Promise<string>(resolve => {
|
|
|
|
grist.rpc.once('message', resolve);
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
|
|
grist.api.render('test_messages_view', 'fullscreen');
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}, FooDescription);
|
|
|
|
grist.ready();
|
|
|
|
},
|
|
|
|
test_messages_view: (grist: GristModule) => {
|
|
|
|
// test if works even if grist.ready() called after postmessage ?
|
|
|
|
grist.ready();
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
|
|
grist.rpc.postMessageForward('test_messages', 'from message view');
|
|
|
|
},
|
|
|
|
test_dispose: (grist: GristModule) => {
|
|
|
|
class MyFoo {
|
|
|
|
public async foo(name: string): Promise<string> {
|
|
|
|
const id = await grist.api.render('test_dispose_view1', 'fullscreen');
|
|
|
|
await grist.api.render('test_dispose_view2', 'fullscreen');
|
|
|
|
await grist.api.dispose(id);
|
|
|
|
return "test";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
grist.rpc.registerImpl<Foo>('grist', new MyFoo(), FooDescription);
|
|
|
|
grist.ready();
|
|
|
|
},
|
|
|
|
test_dispose_view1: (grist) => grist.ready(),
|
|
|
|
test_dispose_view2: (grist) => grist.ready(),
|
|
|
|
test_client_scope: (grist: GristModule) => {
|
|
|
|
class MyFoo {
|
|
|
|
public async foo(name: string): Promise<string> {
|
|
|
|
const stub = grist.rpc.getStub<Storage>('storage');
|
|
|
|
stub.setItem("red", "#f00");
|
|
|
|
stub.setItem("green", "#0f0");
|
|
|
|
stub.setItem("blue", "#00f");
|
|
|
|
return stub.getItem(name);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
grist.rpc.registerImpl<Foo>('grist', new MyFoo(), FooDescription);
|
|
|
|
grist.ready();
|
|
|
|
},
|
|
|
|
test_client_scope_from_view: (grist: GristModule) => {
|
|
|
|
// hit linting limit for number of classes in a single file :-)
|
|
|
|
const myFoo = {
|
|
|
|
foo(name: string): Promise<string> {
|
|
|
|
return new Promise<string> (resolve => {
|
|
|
|
grist.rpc.once("message", (msg: any) => resolve(name + msg));
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
|
|
grist.api.render('view_client_scope', 'fullscreen');
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
grist.rpc.registerImpl<Foo>('grist', myFoo, FooDescription);
|
|
|
|
grist.ready();
|
|
|
|
},
|
|
|
|
test_client_scope_typed: (grist: GristModule) => {
|
|
|
|
const myFoo = {
|
|
|
|
foo(name: string): Promise<string> {
|
|
|
|
const stub = grist.rpc.getStub<any>('storage');
|
|
|
|
return stub.setItem(1); // this should be an error
|
|
|
|
}
|
|
|
|
};
|
|
|
|
grist.rpc.registerImpl<Foo>('grist', myFoo, FooDescription);
|
|
|
|
grist.ready();
|
|
|
|
},
|
|
|
|
view1: (grist: GristModule) => {
|
|
|
|
const myFoo = {
|
|
|
|
async foo(name: string): Promise<string> {
|
|
|
|
return `foo ${name} from view1`;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
grist.rpc.registerImpl<Foo>('foo', myFoo, FooDescription);
|
|
|
|
grist.ready();
|
|
|
|
},
|
|
|
|
view2: (grist: GristModule) => {
|
|
|
|
grist.ready();
|
|
|
|
},
|
|
|
|
view_client_scope: async (grist: GristModule) => {
|
|
|
|
const stub = grist.rpc.getStub<Storage>('storage');
|
|
|
|
grist.ready();
|
|
|
|
stub.setItem("red", "#f00");
|
|
|
|
const result = await stub.getItem("red");
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
|
|
grist.rpc.postMessageForward("test_client_scope_from_view", result);
|
|
|
|
},
|
|
|
|
};
|