gristlabs_grist-core/test/gen-server/ApiServer.ts

2348 lines
95 KiB
TypeScript
Raw Permalink Normal View History

import axios, {AxiosRequestConfig, AxiosResponse} from 'axios';
import * as chai from 'chai';
import omit = require('lodash/omit');
import {configForUser, configWithPermit, getRowCounts as getRowCountsForDb} from 'test/gen-server/testUtils';
import * as testUtils from 'test/server/testUtils';
import {createEmptyOrgUsageSummary, OrgUsageSummary} from 'app/common/DocUsage';
import {Document, Workspace} from 'app/common/UserAPI';
import {Organization} from 'app/gen-server/entity/Organization';
import {Product} from 'app/gen-server/entity/Product';
import {HomeDBManager, UserChange} from 'app/gen-server/lib/HomeDBManager';
import {TestServer} from 'test/gen-server/apiUtils';
import {TEAM_FREE_PLAN} from 'app/common/Features';
const assert = chai.assert;
let server: TestServer;
let dbManager: HomeDBManager;
let homeUrl: string;
let userCountUpdates: {[orgId: number]: number[]} = {};
const chimpy = configForUser('Chimpy');
const kiwi = configForUser('Kiwi');
const charon = configForUser('Charon');
const support = configForUser('Support');
const nobody = configForUser('Anonymous');
const chimpyEmail = 'chimpy@getgrist.com';
const kiwiEmail = 'kiwi@getgrist.com';
const charonEmail = 'charon@getgrist.com';
let chimpyRef = '';
let kiwiRef = '';
let charonRef = '';
async function getRowCounts() {
return getRowCountsForDb(dbManager);
}
describe('ApiServer', function() {
let oldEnv: testUtils.EnvironmentSnapshot;
testUtils.setTmpLogLevel('error');
before(async function() {
oldEnv = new testUtils.EnvironmentSnapshot();
process.env.GRIST_TEMPLATE_ORG = 'templates';
server = new TestServer(this);
homeUrl = await server.start(['home', 'docs']);
dbManager = server.dbManager;
chimpyRef = await dbManager.getUserByLogin(chimpyEmail).then((user) => user!.ref);
kiwiRef = await dbManager.getUserByLogin(kiwiEmail).then((user) => user!.ref);
charonRef = await dbManager.getUserByLogin(charonEmail).then((user) => user!.ref);
// Listen to user count updates and add them to an array.
dbManager.on('userChange', ({org, countBefore, countAfter}: UserChange) => {
if (countBefore === countAfter) { return; }
userCountUpdates[org.id] = userCountUpdates[org.id] || [];
userCountUpdates[org.id].push(countAfter);
});
});
afterEach(async function() {
userCountUpdates = {};
await server.sanityCheck();
});
after(async function() {
oldEnv.restore();
await server.stop();
});
it('GET /api/orgs reports nothing for anonymous without org in url', async function() {
const resp = await axios.get(`${homeUrl}/api/orgs`, nobody);
assert.equal(resp.status, 200);
assert.deepEqual(resp.data, []);
});
it('GET /api/orgs reports nothing for anonymous with unavailable org', async function() {
const resp = await axios.get(`${homeUrl}/o/deep/api/orgs`, nobody);
assert.equal(resp.status, 200);
assert.deepEqual(resp.data, []);
});
for (const users of [['anon'], ['anon', 'everyone'], ['everyone']]) {
it(`GET /api/orgs reports something for anonymous with org available to ${users.join(', ')}`, async function() {
const addUsers: {[key: string]: 'viewers'|'owners'} = {};
const removeUsers: {[key: string]: null} = {};
for (const user of users) {
const email = `${user}@getgrist.com`;
addUsers[email] = 'viewers';
removeUsers[email] = null;
}
// Get id of "Abyss" org (domain name: "deep")
const oid = await dbManager.testGetId('Abyss');
try {
// Only support user has right currently to add/remove everyone@
let resp = await axios.patch(`${homeUrl}/api/orgs/${oid}/access`, {
delta: { users: { 'support@getgrist.com': 'owners' }}
}, charon);
assert.equal(resp.status, 200);
// Make anon@/everyone@ a viewer of Abyss org
resp = await axios.patch(`${homeUrl}/api/orgs/${oid}/access`, {
delta: {users: addUsers}
}, support);
assert.equal(resp.status, 200);
// Confirm that anon now sees this org when using a url mentioning the org
resp = await axios.get(`${homeUrl}/o/deep/api/orgs`, nobody);
assert.equal(resp.status, 200);
assert.deepEqual(["Abyss"], resp.data.map((o: any) => o.name));
// Org is marked as public
assert.equal(resp.data[0].public, true);
// Confirm that anon doesn't see this org from other urls
resp = await axios.get(`${homeUrl}/o/nasa/api/orgs`, nobody);
assert.equal(resp.status, 200);
assert.deepEqual([], resp.data.map((o: any) => o.name));
// Confirm that anon doesn't see this org from /session/access/all
resp = await axios.get(`${homeUrl}/api/session/access/all`,
await server.getCookieLogin('nasa', null));
assert.equal(resp.status, 200);
assert.deepEqual([], resp.data.orgs.map((o: any) => o.name));
// Confirm that regular users don't see this org listed from other domains,
// either via api/orgs or api/session/access/all.
resp = await axios.get(`${homeUrl}/o/nasa/api/orgs`, chimpy);
assert.equal(resp.status, 200);
let orgs = resp.data.map((o: any) => o.name);
assert.notInclude(orgs, 'Abyss');
resp = await axios.get(`${homeUrl}/o/nasa/api/session/access/all`,
await server.getCookieLogin('nasa', {email: 'chimpy@getgrist.com',
name: 'Chimpy'}));
assert.equal(resp.status, 200);
orgs = resp.data.orgs.map((o: any) => o.name);
assert.notInclude(orgs, 'Abyss');
// Confirm that regular users see this org only via api/orgs,
// and only when on the right domain, and only when shared with "everyone@".
resp = await axios.get(`${homeUrl}/o/deep/api/orgs`, chimpy);
assert.equal(resp.status, 200);
orgs = resp.data.map((o: any) => o.name);
if (users.includes('everyone')) {
assert.include(orgs, 'Abyss');
} else {
assert.notInclude(orgs, 'Abyss');
}
resp = await axios.get(`${homeUrl}/o/deep/api/session/access/all`,
await server.getCookieLogin('deep', {email: 'chimpy@getgrist.com',
name: 'Chimpy'}));
assert.equal(resp.status, 200);
orgs = resp.data.orgs.map((o: any) => o.name);
assert.notInclude(orgs, 'Abyss');
} finally {
// Cleanup: remove anon from org
let resp = await axios.patch(`${homeUrl}/api/orgs/${oid}/access`, {
delta: {users: removeUsers}
}, support);
assert.equal(resp.status, 200);
resp = await axios.patch(`${homeUrl}/api/orgs/${oid}/access`, {
delta: { users: { 'support@getgrist.com': null }}
}, charon);
assert.equal(resp.status, 200);
// Confirm that access has gone away
resp = await axios.get(`${homeUrl}/o/deep/api/orgs`, nobody);
assert.equal(resp.status, 200);
assert.deepEqual([], resp.data.map((o: any) => o.name));
}
});
}
it('GET /api/orgs is operational', async function() {
const resp = await axios.get(`${homeUrl}/api/orgs`, chimpy);
assert.equal(resp.status, 200);
assert.deepEqual(resp.data.map((org: any) => org.name),
['Chimpyland', 'EmptyOrg', 'EmptyWsOrg', 'Fish', 'Flightless',
'FreeTeam', 'NASA', 'Primately', 'TestDailyApiLimit']);
// personal orgs should have an owner and no domain
// createdAt and updatedAt are omitted since exact times cannot be predicted.
assert.deepEqual(omit(resp.data[0], 'createdAt', 'updatedAt'), {
id: await dbManager.testGetId('Chimpyland'),
name: 'Chimpyland',
access: 'owners',
// public is not set.
domain: 'docs-1',
host: null,
owner: {
id: await dbManager.testGetId('Chimpy'),
ref: await dbManager.testGetRef('Chimpy'),
name: 'Chimpy',
picture: null
}
});
assert.isNotNull(resp.data[0].updatedAt);
// regular orgs should have a domain and no owner
assert.equal(resp.data[1].domain, 'blankiest');
assert.equal(resp.data[1].owner, null);
});
it('GET /api/orgs respects permissions', async function() {
const resp = await axios.get(`${homeUrl}/api/orgs`, kiwi);
assert.equal(resp.status, 200);
assert.equal(resp.data[0].name, 'Kiwiland');
assert.equal(resp.data[0].owner.name, 'Kiwi');
assert.deepEqual(resp.data.map((org: any) => org.name),
['Kiwiland', 'Fish', 'Flightless', 'Primately']);
});
it('GET /api/orgs/{oid} is operational', async function() {
const oid = await dbManager.testGetId('NASA');
const resp = await axios.get(`${homeUrl}/api/orgs/${oid}`, chimpy);
assert.equal(resp.status, 200);
assert.equal(resp.data.name, 'NASA');
});
it('GET /api/orgs/{oid} accepts domains', async function() {
const resp = await axios.get(`${homeUrl}/api/orgs/nasa`, chimpy);
assert.equal(resp.status, 200);
assert.equal(resp.data.name, 'NASA');
});
it('GET /api/orgs/{oid} accepts current keyword', async function() {
const resp = await axios.get(`${homeUrl}/o/nasa/api/orgs/current`, chimpy);
assert.equal(resp.status, 200);
assert.equal(resp.data.name, 'NASA');
});
it('GET /api/orgs/{oid} fails with current keyword if no domain active', async function() {
const resp = await axios.get(`${homeUrl}/api/orgs/current`, chimpy);
assert.equal(resp.status, 400);
});
it('GET /api/orgs/{oid} returns owner when available', async function() {
const oid = await dbManager.testGetId('Chimpyland');
const resp = await axios.get(`${homeUrl}/api/orgs/${oid}`, chimpy);
assert.equal(resp.status, 200);
// billingAccount is omitted since it isn't focus of this test.
assert.deepEqual(omit(resp.data, 'createdAt', 'updatedAt', 'billingAccount'), {
id: oid,
name: 'Chimpyland',
domain: 'docs-1',
host: null,
access: 'owners',
owner: {
id: await dbManager.testGetId('Chimpy'),
ref: await dbManager.testGetRef('Chimpy'),
name: 'Chimpy',
picture: null
}
});
assert.isNotNull(resp.data.updatedAt);
});
it('GET /api/orgs/{oid} respects permissions', async function() {
const oid = await dbManager.testGetId('Kiwiland');
const resp = await axios.get(`${homeUrl}/api/orgs/${oid}`, chimpy);
assert.equal(resp.status, 403);
assert.deepEqual(resp.data, {error: "access denied"});
});
it('GET /api/orgs/{oid} returns 404 appropriately', async function() {
const resp = await axios.get(`${homeUrl}/api/orgs/9999`, chimpy);
assert.equal(resp.status, 404);
assert.deepEqual(resp.data, {error: "organization not found"});
});
it('GET /api/orgs/{oid}/workspaces is operational', async function() {
const oid = await dbManager.testGetId('NASA');
const resp = await axios.get(`${homeUrl}/api/orgs/${oid}/workspaces`, chimpy);
assert.equal(resp.status, 200);
assert.lengthOf(resp.data, 2);
assert.deepEqual(resp.data.map((ws: any) => ws.name), ['Horizon', 'Rovers']);
assert.equal(resp.data[0].id, await dbManager.testGetId('Horizon'));
assert.equal(resp.data[1].id, await dbManager.testGetId('Rovers'));
assert.deepEqual(resp.data[0].docs.map((doc: any) => doc.name),
['Jupiter', 'Pluto', 'Beyond']);
// Check that Primately access is as expected.
const oid2 = await dbManager.testGetId('Primately');
const resp2 = await axios.get(`${homeUrl}/api/orgs/${oid2}/workspaces`, kiwi);
assert.equal(resp2.status, 200);
assert.lengthOf(resp2.data, 2);
assert.deepEqual(resp2.data.map((ws: any) => ws.name), ['Fruit', 'Trees']);
assert.deepEqual(resp2.data[0].docs.map((doc: any) => omit(doc, 'createdAt', 'updatedAt')), [{
access: "viewers",
// public is not set
id: "sampledocid_6",
name: "Bananas",
isPinned: false,
urlId: null,
trunkId: null,
type: null,
forks: [],
}, {
access: "viewers",
// public is not set
id: "sampledocid_7",
name: "Apples",
isPinned: false,
urlId: null,
trunkId: null,
type: null,
forks: [],
}]);
assert.deepEqual(resp2.data[1].docs.map((doc: any) => omit(doc, 'createdAt', 'updatedAt')), [{
access: "viewers",
id: "sampledocid_8",
name: "Tall",
isPinned: false,
urlId: null,
trunkId: null,
type: null,
forks: [],
}, {
access: "viewers",
id: "sampledocid_9",
name: "Short",
isPinned: false,
urlId: null,
trunkId: null,
type: null,
forks: [],
}]);
// Assert that updatedAt values are present.
resp2.data[0].docs.map((doc: any) => assert.isNotNull(doc.updatedAt));
resp2.data[1].docs.map((doc: any) => assert.isNotNull(doc.updatedAt));
});
it('GET /api/orgs/{oid}/workspaces accepts domains', async function() {
const resp = await axios.get(`${homeUrl}/api/orgs/nasa/workspaces`, chimpy);
assert.equal(resp.status, 200);
assert.lengthOf(resp.data, 2);
assert.deepEqual(resp.data.map((ws: any) => ws.name), ['Horizon', 'Rovers']);
});
it('GET /api/orgs/{oid}/workspaces accepts current keyword', async function() {
const resp = await axios.get(`${homeUrl}/o/nasa/api/orgs/current/workspaces`, chimpy);
assert.equal(resp.status, 200);
assert.lengthOf(resp.data, 2);
assert.deepEqual(resp.data.map((ws: any) => ws.name), ['Horizon', 'Rovers']);
});
it('GET /api/orgs/{oid}/workspaces returns 403 appropriately', async function() {
const oid = await dbManager.testGetId('Kiwiland');
const resp = await axios.get(`${homeUrl}/api/orgs/${oid}/workspaces`, chimpy);
assert.equal(resp.status, 403);
});
it('GET /api/orgs/{oid}/workspaces lists individually shared workspaces', async function() {
const oid = await dbManager.testGetId('Primately');
const resp = await axios.get(`${homeUrl}/api/orgs/${oid}/workspaces`, chimpy);
assert.equal(resp.status, 200);
assert.lengthOf(resp.data, 1); // 1 of 2 workspaces should be present
assert.equal(resp.data[0].name, 'Fruit');
});
it('GET /api/orgs/{oid}/workspaces lists individually shared docs', async function() {
const oid = await dbManager.testGetId('Flightless');
const resp = await axios.get(`${homeUrl}/api/orgs/${oid}/workspaces`, chimpy);
assert.equal(resp.status, 200);
assert.lengthOf(resp.data, 1);
assert.equal(resp.data[0].name, 'Media');
assert.lengthOf(resp.data[0].docs, 1); // 1 of 2 docs should be available
assert.equal(resp.data[0].docs[0].name, 'Antartic');
});
it('GET /api/orgs/{wid}/workspaces gives results when workspace is empty', async function() {
const oid = await dbManager.testGetId('EmptyWsOrg');
const resp = await axios.get(`${homeUrl}/api/orgs/${oid}/workspaces`, chimpy);
assert.equal(resp.status, 200);
assert.equal(resp.data[0].name, 'Vacuum');
assert.lengthOf(resp.data[0].docs, 0); // No docs present
});
it('GET /api/workspaces/{wid} is operational', async function() {
const wid = await dbManager.testGetId('Horizon');
const resp = await axios.get(`${homeUrl}/api/workspaces/${wid}`, chimpy);
assert.deepEqual(resp.data.docs.map((doc: any) => doc.name),
['Jupiter', 'Pluto', 'Beyond']);
assert.equal(resp.data.org.name, 'NASA');
assert.equal(resp.data.org.owner, null);
});
it('GET /api/workspaces/{wid} lists individually shared docs', async function() {
const wid = await dbManager.testGetId('Media');
const resp = await axios.get(`${homeUrl}/api/workspaces/${wid}`, chimpy);
assert.equal(resp.status, 200);
assert.lengthOf(resp.data.docs, 1); // 1 of 2 docs should be available
assert.equal(resp.data.docs[0].name, 'Antartic');
});
it('GET /api/workspaces/{wid} gives results when empty', async function() {
const wid = await dbManager.testGetId('Vacuum');
const resp = await axios.get(`${homeUrl}/api/workspaces/${wid}`, chimpy);
assert.equal(resp.status, 200);
assert.equal(resp.data.name, 'Vacuum');
assert.lengthOf(resp.data.docs, 0); // No docs present
});
it('GET /api/workspaces/{wid} respects permissions', async function() {
const wid = await dbManager.testGetId('Deep');
const resp = await axios.get(`${homeUrl}/api/workspaces/${wid}`, chimpy);
assert.equal(resp.status, 403);
});
it('GET /api/workspaces/{wid} returns 404 appropriately', async function() {
const resp = await axios.get(`${homeUrl}/api/workspaces/9999`, chimpy);
assert.equal(resp.status, 404);
});
it('GET /api/workspaces/{wid} gives owner of org', async function() {
const wid = await dbManager.testGetId('Private');
const resp = await axios.get(`${homeUrl}/api/workspaces/${wid}`, charon);
assert.equal(resp.data.org.owner.name, 'Chimpy');
});
it('POST /api/orgs/{oid}/workspaces is operational', async function() {
// Add a 'Planets' workspace to the 'NASA' org.
const oid = await dbManager.testGetId('NASA');
const wid = await getNextId(dbManager, 'workspaces');
const resp = await axios.post(`${homeUrl}/api/orgs/${oid}/workspaces`, {
name: 'Planets'
}, chimpy);
// Assert that the response is successful and contains the next available workspace id.
assert.equal(resp.status, 200);
assert.equal(resp.data, wid);
// Assert that the added workspace can be fetched and returns as expected.
const fetchResp = await axios.get(`${homeUrl}/api/workspaces/${resp.data}`, chimpy);
const workspace = omit(fetchResp.data, 'createdAt', 'updatedAt');
workspace.org = omit(workspace.org, 'createdAt', 'updatedAt');
assert.deepEqual(workspace, {
id: wid,
name: 'Planets',
access: 'owners',
docs: [],
isSupportWorkspace: false,
org: {
id: 1,
name: 'NASA',
domain: 'nasa',
host: null,
owner: null
}
});
});
it('POST /api/orgs/{oid}/workspaces returns 404 appropriately', async function() {
// Attempt to add to an org that doesn't exist.
const resp = await axios.post(`${homeUrl}/api/orgs/9999/workspaces`, {
name: 'Planets'
}, chimpy);
assert.equal(resp.status, 404);
});
it('POST /api/orgs/{oid}/workspaces returns 403 appropriately', async function() {
// Attempt to add to an org that chimpy doesn't have write permission on.
const oid = await dbManager.testGetId('Primately');
const resp = await axios.post(`${homeUrl}/api/orgs/${oid}/workspaces`, {
name: 'Apes'
}, chimpy);
assert.equal(resp.status, 403);
});
it('POST /api/orgs/{oid}/workspaces returns 400 appropriately', async function() {
// Use an unknown property and check that the operation fails with status 400.
const oid = await dbManager.testGetId('NASA');
const resp = await axios.post(`${homeUrl}/api/orgs/${oid}/workspaces`, {x: 1}, chimpy);
assert.equal(resp.status, 400);
});
it('PATCH /api/workspaces/{wid} is operational', async function() {
// Rename the 'Horizons' workspace to 'Horizons2'.
const wid = await dbManager.testGetId('Horizon');
const resp = await axios.patch(`${homeUrl}/api/workspaces/${wid}`, {
name: 'Horizon2'
}, chimpy);
// Assert that the response is successful.
assert.equal(resp.status, 200);
// Assert that the workspace was renamed as expected.
const fetchResp = await axios.get(`${homeUrl}/api/workspaces/${wid}`, chimpy);
const workspace = fetchResp.data;
assert.equal(workspace.name, 'Horizon2');
// Change the name back.
const wid2 = await dbManager.testGetId('Horizon2');
const resp2 = await axios.patch(`${homeUrl}/api/workspaces/${wid2}`, {
name: 'Horizon'
}, chimpy);
assert.equal(resp2.status, 200);
});
it('PATCH /api/workspaces/{wid} returns 404 appropriately', async function() {
// Attempt to rename a workspace that doesn't exist.
const resp = await axios.patch(`${homeUrl}/api/workspaces/9999`, {
name: 'Rename'
}, chimpy);
assert.equal(resp.status, 404);
});
it('PATCH /api/workspaces/{wid} returns 403 appropriately', async function() {
// Attempt to rename a workspace without UPDATE access.
const wid = await dbManager.testGetId('Fruit');
const resp = await axios.patch(`${homeUrl}/api/workspaces/${wid}`, {
name: 'Fruit2'
}, chimpy);
assert.equal(resp.status, 403);
});
it('PATCH /api/workspaces/{wid} returns 400 appropriately', async function() {
// Use an unavailable property and check that the operation fails with 400.
const wid = await dbManager.testGetId('Rovers');
const resp = await axios.patch(`${homeUrl}/api/workspaces/${wid}`, {x: 1}, chimpy);
assert.equal(resp.status, 400);
});
it('DELETE /api/workspaces/{wid} is operational', async function() {
// Add Kiwi to 'Public' workspace.
const oid = await dbManager.testGetId('Chimpyland');
let wid = await dbManager.testGetId('Public');
// Assert that the number of users in the org has not been updated.
assert.deepEqual(userCountUpdates[oid as number], undefined);
const delta = {
users: {[kiwiEmail]: 'viewers'}
};
const accessResp = await axios.patch(`${homeUrl}/api/workspaces/${wid}/access`, {delta}, chimpy);
assert.equal(accessResp.status, 200);
// Assert that Kiwi is a guest of the org.
const orgResp = await axios.get(`${homeUrl}/api/orgs/${oid}/access`, chimpy);
assert.equal(orgResp.status, 200);
assert.deepEqual(orgResp.data, {
users: [{
id: 1,
name: 'Chimpy',
email: chimpyEmail,
ref: chimpyRef,
picture: null,
access: "owners",
isMember: true,
}, {
id: 2,
name: 'Kiwi',
email: kiwiEmail,
ref: kiwiRef,
picture: null,
access: "guests",
isMember: false,
}, {
id: 3,
name: 'Charon',
email: charonEmail,
ref: charonRef,
picture: null,
access: "viewers",
isMember: true,
}]
});
// Assert that the number of non-guest users in the org is unchanged.
assert.deepEqual(userCountUpdates[oid as number], undefined);
const beforeDelCount = await getRowCounts();
// Delete 'Public' workspace.
const resp = await axios.delete(`${homeUrl}/api/workspaces/${wid}`, chimpy);
// Assert that the response is successful.
assert.equal(resp.status, 200);
// Assert that the workspace is no longer in the database.
const fetchResp = await axios.get(`${homeUrl}/api/workspaces/${wid}`, chimpy);
assert.equal(fetchResp.status, 404);
// Assert that Kiwi is no longer a guest of the org.
const orgResp2 = await axios.get(`${homeUrl}/api/orgs/${oid}/access`, chimpy);
assert.equal(orgResp2.status, 200);
assert.deepEqual(orgResp2.data, {
users: [{
id: 1,
name: 'Chimpy',
email: chimpyEmail,
ref: chimpyRef,
picture: null,
access: "owners",
isMember: true,
}, {
id: 3,
name: 'Charon',
email: charonEmail,
ref: charonRef,
picture: null,
access: "viewers",
isMember: true,
}]
});
// Assert that the number of non-guest users in the org remains unchanged.
assert.deepEqual(userCountUpdates[oid as number], undefined);
const afterDelCount1 = await getRowCounts();
// Assert that one workspace was removed.
assert.equal(afterDelCount1.workspaces, beforeDelCount.workspaces - 1);
// Assert that Kiwi's workspace viewer and org guest items were removed.
assert.equal(afterDelCount1.groupUsers, beforeDelCount.groupUsers - 2);
// Re-add 'Public'
const addWsResp = await axios.post(`${homeUrl}/api/orgs/${oid}/workspaces`, {
name: 'Public'
}, chimpy);
// Assert that the response is successful
assert.equal(addWsResp.status, 200);
wid = addWsResp.data;
// Add a doc to 'Public'
const addDocResp1 = await axios.post(`${homeUrl}/api/workspaces/${wid}/docs`, {
name: 'PublicDoc1'
}, chimpy);
// Add another workspace, 'Public2'
const addWsResp2 = await axios.post(`${homeUrl}/api/orgs/${oid}/workspaces`, {
name: 'Public2'
}, chimpy);
assert.equal(addWsResp2.status, 200);
// Get 'Public2' workspace.
const wid2 = addWsResp2.data;
// Add a doc to 'Public2'
const addDocResp2 = await axios.post(`${homeUrl}/api/workspaces/${wid2}/docs`, {
name: 'PublicDoc2'
}, chimpy);
assert.equal(addDocResp2.status, 200);
// Get both doc's ids.
const did1 = addDocResp1.data;
const did2 = addDocResp2.data;
const beforeAddCount = await getRowCounts();
// Add Kiwi to the docs
const docAccessResp1 = await axios.patch(`${homeUrl}/api/docs/${did1}/access`, {delta}, chimpy);
assert.equal(docAccessResp1.status, 200);
const docAccessResp2 = await axios.patch(`${homeUrl}/api/docs/${did2}/access`, {delta}, chimpy);
assert.equal(docAccessResp2.status, 200);
// Assert that Kiwi is a guest of the org.
const orgResp3 = await axios.get(`${homeUrl}/api/orgs/${oid}/access`, chimpy);
assert.equal(orgResp3.status, 200);
assert.deepEqual(orgResp3.data, {
users: [{
id: 1,
name: 'Chimpy',
email: chimpyEmail,
ref: chimpyRef,
picture: null,
access: "owners",
isMember: true,
}, {
id: 2,
name: 'Kiwi',
email: kiwiEmail,
ref: kiwiRef,
picture: null,
access: "guests",
isMember: false,
}, {
id: 3,
name: 'Charon',
email: charonEmail,
ref: charonRef,
picture: null,
access: "viewers",
isMember: true,
}]
});
// Assert that the number of non-guest users in the org remains unchanged.
assert.deepEqual(userCountUpdates[oid as number], undefined);
const afterAddCount = await getRowCounts();
// Assert that Kiwi's 2 doc viewer, 2 workspace guest and 1 org guest items were added.
assert.equal(afterAddCount.groupUsers, beforeAddCount.groupUsers + 5);
// Delete 'Public2' workspace.
const deleteResp2 = await axios.delete(`${homeUrl}/api/workspaces/${wid2}`, chimpy);
assert.equal(deleteResp2.status, 200);
const afterDelCount2 = await getRowCounts();
// Assert that one workspace was removed.
assert.equal(afterDelCount2.workspaces, afterAddCount.workspaces - 1);
// Assert that one doc was removed.
assert.equal(afterDelCount2.docs, afterAddCount.docs - 1);
// Assert that one of Kiwi's doc viewer items and one of Kiwi's workspace guest items were removed
// and guest chimpy assignment to org and chimpy owner assignment to doc and ws.
assert.equal(afterDelCount2.groupUsers, afterAddCount.groupUsers - 2 - 3);
// Assert that Kiwi is STILL a guest of the org, since Kiwi is still in the 'Public' doc.
const orgResp4 = await axios.get(`${homeUrl}/api/orgs/${oid}/access`, chimpy);
assert.equal(orgResp4.status, 200);
assert.deepEqual(orgResp4.data, {
users: [{
id: 1,
name: 'Chimpy',
email: chimpyEmail,
ref: chimpyRef,
picture: null,
access: "owners",
isMember: true,
}, {
id: 2,
name: 'Kiwi',
email: kiwiEmail,
ref: kiwiRef,
picture: null,
access: "guests",
isMember: false,
}, {
id: 3,
name: 'Charon',
email: charonEmail,
ref: charonRef,
picture: null,
access: "viewers",
isMember: true,
}]
});
// Assert that the number of non-guest users in the org remains unchanged.
assert.deepEqual(userCountUpdates[oid as number], undefined);
// Delete 'Public' workspace.
const deleteResp3 = await axios.delete(`${homeUrl}/api/workspaces/${wid}`, chimpy);
// Assert that the response is successful.
assert.equal(deleteResp3.status, 200);
const afterDelCount3 = await getRowCounts();
// Assert that another workspace was removed.
assert.equal(afterDelCount3.workspaces, afterDelCount2.workspaces - 1);
// Assert that another doc was removed.
assert.equal(afterDelCount3.docs, afterDelCount2.docs - 1);
// Assert that Kiwi's doc viewer item, workspace guest and org guest items were removed, and Chimpy was
// removed from doc as owner, ws as guest and owner, and org as guest.
assert.equal(afterDelCount3.groupUsers, afterDelCount2.groupUsers - 7);
// Assert that Kiwi is no longer a guest of the org.
const orgResp5 = await axios.get(`${homeUrl}/api/orgs/${oid}/access`, chimpy);
assert.equal(orgResp5.status, 200);
assert.deepEqual(orgResp5.data, {
users: [{
id: 1,
name: 'Chimpy',
email: chimpyEmail,
ref: chimpyRef,
picture: null,
access: "owners",
isMember: true,
}, {
id: 3,
name: 'Charon',
email: charonEmail,
ref: charonRef,
picture: null,
access: "viewers",
isMember: true,
}]
});
// Assert that the number of non-guest users in the org remains unchanged.
assert.deepEqual(userCountUpdates[oid as number], undefined);
// Re-add 'Public' finally
const addWsResp3 = await axios.post(`${homeUrl}/api/orgs/${oid}/workspaces`, {
name: 'Public'
}, chimpy);
// Assert that the response is successful
assert.equal(addWsResp3.status, 200);
});
it('DELETE /api/workspaces/{wid} returns 404 appropriately', async function() {
// Attempt to delete a workspace that doesn't exist.
const resp = await axios.delete(`${homeUrl}/api/workspaces/9999`, chimpy);
assert.equal(resp.status, 404);
});
it('DELETE /api/workspaces/{wid} returns 403 appropriately', async function() {
// Attempt to delete a workspace without REMOVE access.
const wid = await dbManager.testGetId('Fruit');
const resp = await axios.delete(`${homeUrl}/api/workspaces/${wid}`, chimpy);
assert.equal(resp.status, 403);
});
it('POST /api/workspaces/{wid}/docs is operational', async function() {
// Add a 'Surprise' doc to the 'Rovers' workspace.
const wid = await dbManager.testGetId('Rovers');
const resp = await axios.post(`${homeUrl}/api/workspaces/${wid}/docs`, {
name: 'Surprise'
}, chimpy);
// Assert that the response is successful and contains the doc id.
assert.equal(resp.status, 200);
assert.equal(resp.data.length, 22); // The length of a short-uuid string
// Assert that the added doc can be fetched and returns as expected.
const fetchResp = await axios.get(`${homeUrl}/api/workspaces/${wid}`, chimpy);
const workspace = fetchResp.data;
assert.deepEqual(workspace.name, 'Rovers');
assert.deepEqual(workspace.docs.map((d: any) => d.name),
['Curiosity', 'Apathy', 'Surprise']);
});
it('POST /api/workspaces/{wid}/docs handles urlIds', async function() {
// Add a 'Boredom' doc to the 'Rovers' workspace.
const wid = await dbManager.testGetId('Rovers');
let resp = await axios.post(`${homeUrl}/api/workspaces/${wid}/docs`, {
name: 'Boredom',
urlId: 'Hohum'
}, chimpy);
// Assert that the response is successful
assert.equal(resp.status, 200);
assert.equal(resp.data.length, 22); // The length of a short-uuid string
const docId = resp.data;
// Assert that the added doc can be fetched and returns as expected using urlId.
resp = await axios.get(`${homeUrl}/api/docs/Hohum`, chimpy);
assert.equal(resp.status, 200);
assert.equal(resp.data.id, docId);
// Adding a new doc with the same urlId should fail.
resp = await axios.post(`${homeUrl}/api/workspaces/${wid}/docs`, {
name: 'NonEnthusiasm',
urlId: 'Hohum'
}, chimpy);
assert.equal(resp.status, 400);
// Change Boredom doc to use a different urlId
// Also, use the existing urlId in the endpoint just to check that works
resp = await axios.patch(`${homeUrl}/api/docs/Hohum`, {
urlId: 'sigh'
}, chimpy);
assert.equal(resp.status, 200);
// Hohum still resolves to Boredom for the moment
resp = await axios.get(`${homeUrl}/api/docs/Hohum`, chimpy);
assert.equal(resp.data.id, docId);
// Adding a new doc with the Hohum urlId should now succeed.
resp = await axios.post(`${homeUrl}/api/workspaces/${wid}/docs`, {
name: 'NonEnthusiasm',
urlId: 'Hohum'
}, chimpy);
assert.equal(resp.status, 200);
const docId2 = resp.data;
// Hohum now resolves to the new doc.
resp = await axios.get(`${homeUrl}/api/docs/Hohum`, chimpy);
assert.equal(resp.data.id, docId2);
// Delete the new doc. Use urlId to check that works.
await axios.delete(`${homeUrl}/api/docs/Hohum`, chimpy);
});
it('POST /api/workspaces/{wid}/docs returns 404 appropriately', async function() {
// Attempt to add to an workspace that doesn't exist.
const resp = await axios.post(`${homeUrl}/api/workspaces/9999/docs`, {
name: 'Mercury'
}, chimpy);
assert.equal(resp.status, 404);
});
it('POST /api/workspaces/{wid}/docs returns 403 without access', async function() {
// Attempt to add to a workspace that chimpy doesn't have any access to.
const wid = await dbManager.testGetId('Trees');
const resp = await axios.post(`${homeUrl}/api/workspaces/${wid}/docs`, {
name: 'Bushy'
}, chimpy);
assert.equal(resp.status, 403);
});
it('POST /api/workspaces/{wid}/docs returns 403 with view access', async function() {
// Attempt to add to a workspace that chimpy has only view access to.
const wid = await dbManager.testGetId('Fruit');
const resp = await axios.post(`${homeUrl}/api/workspaces/${wid}/docs`, {
name: 'Oranges'
}, chimpy);
assert.equal(resp.status, 403);
});
it('POST /api/workspaces/{wid}/docs returns 400 appropriately', async function() {
// Omit the new doc name and check that the operation fails with status 400.
const wid = await dbManager.testGetId('Rovers');
const resp = await axios.post(`${homeUrl}/api/workspaces/${wid}/docs`, {}, chimpy);
assert.equal(resp.status, 400);
});
it('POST /api/orgs is operational', async function() {
const oid = await getNextId(dbManager, 'orgs');
// Add a 'Magic' org.
const resp = await axios.post(`${homeUrl}/api/orgs`, {
name: 'Magic',
domain: 'magic',
}, chimpy);
// Assert that the response is successful and contains the next available org id.
assert.equal(resp.status, 200);
assert.equal(resp.data, oid);
// Assert that the added org can be fetched and returns as expected.
let fetchResp = await axios.get(`${homeUrl}/api/orgs/${resp.data}`, chimpy);
const org = fetchResp.data;
assert.deepEqual(omit(org, 'createdAt', 'updatedAt'), {
id: oid,
name: 'Magic',
access: 'owners',
domain: `o-${oid}`, // default product suppresses vanity domains
host: null,
owner: null,
billingAccount: {
id: oid,
inGoodStanding: true,
individual: false,
isManager: true,
paid: false,
product: {
id: await dbManager.testGetId('stub'),
features: {},
name: 'stub',
},
status: null,
externalId: null,
externalOptions: null,
features: null,
stripePlanId: null,
paymentLink: null,
},
});
assert.isNotNull(org.updatedAt);
// Upgrade this org to a fancier plan, and check that vanity domain starts working
const db = dbManager.connection.manager;
const dbOrg = await db.findOne(Organization,
{where: {name: 'Magic'},
relations: ['billingAccount', 'billingAccount.product']});
if (!dbOrg) { throw new Error('cannot find Magic'); }
const product = await db.findOne(Product, {where: {name: 'team'}});
if (!product) { throw new Error('cannot find product'); }
dbOrg.billingAccount.product = product;
await dbOrg.billingAccount.save();
// Check that we now get the vanity domain
fetchResp = await axios.get(`${homeUrl}/api/orgs/${oid}`, chimpy);
assert.equal(fetchResp.data.domain, 'magic');
});
it('POST /api/orgs returns 400 appropriately', async function() {
// Omit the new org name and check that the operation fails with status 400.
const resp = await axios.post(`${homeUrl}/api/orgs`, {
domain: 'invalid-req'
}, chimpy);
assert.equal(resp.status, 400);
});
it('PATCH /api/orgs/{oid} is operational', async function() {
// Rename the 'Magic' org to 'Holiday' with domain 'holiday'.
const oid = await dbManager.testGetId('Magic');
const resp = await axios.patch(`${homeUrl}/api/orgs/${oid}`, {
name: 'Holiday',
domain: 'holiday'
}, chimpy);
// Assert that the response is successful.
assert.equal(resp.status, 200);
// Assert that the org was renamed as expected.
const fetchResp = await axios.get(`${homeUrl}/api/orgs/${oid}`, chimpy);
const org = fetchResp.data;
assert.equal(org.name, 'Holiday');
assert.equal(org.domain, 'holiday');
// Update the org domain to 'holiday2'.
const resp2 = await axios.patch(`${homeUrl}/api/orgs/${oid}`, {
domain: 'holiday2'
}, chimpy);
// Assert that the response is successful.
assert.equal(resp2.status, 200);
// Assert that the org was updated as expected.
const fetchResp2 = await axios.get(`${homeUrl}/api/orgs/${oid}`, chimpy);
assert.equal(fetchResp2.data.name, 'Holiday');
assert.equal(fetchResp2.data.domain, 'holiday2');
});
it('PATCH /api/orgs/{oid} returns 404 appropriately', async function() {
// Attempt to rename an org that doesn't exist.
const resp = await axios.patch(`${homeUrl}/api/orgs/9999`, {
name: 'Rename'
}, chimpy);
assert.equal(resp.status, 404);
});
it('PATCH /api/orgs/{oid} returns 403 appropriately', async function() {
// Attempt to rename an org without UPDATE access.
const oid = await dbManager.testGetId('Primately');
const resp = await axios.patch(`${homeUrl}/api/orgs/${oid}`, {
name: 'Primately2'
}, chimpy);
assert.equal(resp.status, 403);
});
it('PATCH /api/orgs/{oid} returns 400 appropriately', async function() {
// Use an unavailable property and check that the operation fails with 400.
const oid = await dbManager.testGetId('Holiday');
const resp = await axios.patch(`${homeUrl}/api/orgs/${oid}`, {x: 1}, chimpy);
assert.equal(resp.status, 400);
assert.match(resp.data.error, /unrecognized property/);
});
it('DELETE /api/orgs/{oid} is operational', async function() {
// Delete the 'Holiday' org.
const oid = await dbManager.testGetId('Holiday');
const resp = await axios.delete(`${homeUrl}/api/orgs/${oid}`, chimpy);
// Assert that the response is successful.
assert.equal(resp.status, 200);
// Assert that the org is no longer in the database.
const fetchResp = await axios.get(`${homeUrl}/api/orgs/${oid}`, chimpy);
assert.equal(fetchResp.status, 404);
});
it('DELETE /api/orgs/{oid} returns 404 appropriately', async function() {
// Attempt to delete an org that doesn't exist.
const resp = await axios.delete(`${homeUrl}/api/orgs/9999`, chimpy);
assert.equal(resp.status, 404);
});
it('DELETE /api/orgs/{oid} returns 403 appropriately', async function() {
// Attempt to delete an org without REMOVE access.
const oid = await dbManager.testGetId('Primately');
const resp = await axios.delete(`${homeUrl}/api/orgs/${oid}`, chimpy);
assert.equal(resp.status, 403);
});
it('GET /api/docs/{did} is operational', async function() {
const did = await dbManager.testGetId('Jupiter');
const resp = await axios.get(`${homeUrl}/api/docs/${did}`, chimpy);
assert.equal(resp.status, 200);
const doc: Document = resp.data;
assert.equal(doc.name, 'Jupiter');
assert.equal(doc.workspace.name, 'Horizon');
assert.equal(doc.workspace.org.name, 'NASA');
assert.equal(doc.public, undefined);
});
it('GET /api/docs/{did} returns 404 for nonexistent doc', async function() {
const resp = await axios.get(`${homeUrl}/api/docs/typotypotypo`, chimpy);
assert.equal(resp.status, 404);
});
it('GET /api/docs/{did} returns 403 without access', async function() {
const did = await dbManager.testGetId('Jupiter');
const resp = await axios.get(`${homeUrl}/api/docs/${did}`, kiwi);
assert.equal(resp.status, 403);
});
// Unauthorized folks can currently check if a document uuid exists and that's ok,
// arguably, because uuids don't leak anything sensitive.
it('GET /api/docs/{did} returns 404 without org access for nonexistent doc', async function() {
const resp = await axios.get(`${homeUrl}/api/docs/typotypotypo`, kiwi);
assert.equal(resp.status, 404);
});
it('GET /api/docs/{did} returns 404 for doc accessed from wrong org', async function() {
const did = await dbManager.testGetId('Jupiter');
const resp = await axios.get(`${homeUrl}/o/pr/api/docs/${did}`, chimpy);
assert.equal(resp.status, 404);
});
it('GET /api/docs/{did} works for doc accessed from correct org', async function() {
const did = await dbManager.testGetId('Jupiter');
const resp = await axios.get(`${homeUrl}/o/nasa/api/docs/${did}`, chimpy);
assert.equal(resp.status, 200);
});
it('PATCH /api/docs/{did} is operational', async function() {
// Rename the 'Surprise' doc to 'Surprise2'.
const did = await dbManager.testGetId('Surprise');
const resp = await axios.patch(`${homeUrl}/api/docs/${did}`, {
name: 'Surprise2'
}, chimpy);
// Assert that the response is successful.
assert.equal(resp.status, 200);
// Assert that the doc was renamed as expected.
const wid = await dbManager.testGetId('Rovers');
const fetchResp = await axios.get(`${homeUrl}/api/workspaces/${wid}`, chimpy);
const workspace = fetchResp.data;
assert.deepEqual(workspace.name, 'Rovers');
assert.deepEqual(workspace.docs.map((d: any) => d.name),
['Curiosity', 'Apathy', 'Surprise2', 'Boredom']);
});
it('PATCH /api/docs/{did} works for urlIds', async function() {
// Check that 'curio' is not yet a valid id for anything
let resp = await axios.get(`${homeUrl}/api/docs/curio`, chimpy);
assert.equal(resp.status, 404);
// Make 'curio' a urlId for document named 'Curiosity'
const did = await dbManager.testGetId('Curiosity');
resp = await axios.patch(`${homeUrl}/api/docs/${did}`, {
urlId: 'curio'
}, chimpy);
// Assert that the response is successful.
assert.equal(resp.status, 200);
// Check we can now access same doc via urlId.
resp = await axios.get(`${homeUrl}/api/docs/curio`, chimpy);
assert.equal(resp.status, 200);
assert.equal(resp.data.id, did);
assert.equal(resp.data.urlId, 'curio');
// Check that we still have access via docId.
resp = await axios.get(`${homeUrl}/api/docs/${did}`, chimpy);
assert.equal(resp.status, 200);
assert.equal(resp.data.id, did);
assert.equal(resp.data.urlId, 'curio');
// Add another urlId for the same doc
resp = await axios.patch(`${homeUrl}/api/docs/${did}`, {
urlId: 'hmm'
}, chimpy);
// Check we can now access same doc via this new urlId.
resp = await axios.get(`${homeUrl}/api/docs/hmm`, chimpy);
assert.equal(resp.status, 200);
assert.equal(resp.data.id, did);
assert.equal(resp.data.urlId, 'hmm');
// Check that urlIds accumulate, and previous urlId still works.
resp = await axios.get(`${homeUrl}/api/docs/curio`, chimpy);
assert.equal(resp.status, 200);
assert.equal(resp.data.id, did);
assert.equal(resp.data.urlId, 'hmm');
// Check that we still have access via docId.
resp = await axios.get(`${homeUrl}/api/docs/${did}`, chimpy);
assert.equal(resp.status, 200);
assert.equal(resp.data.id, did);
assert.equal(resp.data.urlId, 'hmm');
});
it('PATCH /api/docs/{did} handles urlIds for different orgs independently', async function() {
// set a urlId with the same name on two docs in different orgs
const did = await dbManager.testGetId('Curiosity'); // part of NASA org
const did2 = await dbManager.testGetId('Herring'); // part of Fish org
let resp = await axios.patch(`${homeUrl}/api/docs/${did}`, {
urlId: 'example'
}, chimpy);
assert.equal(resp.status, 200);
resp = await axios.patch(`${homeUrl}/api/docs/${did2}`, {
urlId: 'example'
}, chimpy);
assert.equal(resp.status, 200);
// Check that we get the right doc in the right org.
resp = await axios.get(`${homeUrl}/o/nasa/api/docs/example`, chimpy);
assert.equal(resp.data.id, did);
resp = await axios.get(`${homeUrl}/o/fish/api/docs/example`, chimpy);
assert.equal(resp.data.id, did2);
// For a url that isn't associated with an org, the result is ambiguous for this user.
resp = await axios.get(`${homeUrl}/api/docs/example`, chimpy);
assert.equal(resp.status, 400);
// For user Kiwi, who has no NASA access, the result is not ambiguous.
resp = await axios.get(`${homeUrl}/api/docs/example`, kiwi);
assert.equal(resp.status, 200);
});
it('PATCH /api/docs/{did} can reuse urlIds within an org', async function() {
// Make 'puzzler' a urlId for document named 'Curiosity'
const did = await dbManager.testGetId('Curiosity');
let resp = await axios.patch(`${homeUrl}/api/docs/${did}`, {urlId: 'puzzler'}, chimpy);
// Assert that the response is successful.
assert.equal(resp.status, 200);
// Check we can now access same doc via urlId.
resp = await axios.get(`${homeUrl}/api/docs/puzzler`, chimpy);
assert.equal(resp.status, 200);
assert.equal(resp.data.id, did);
assert.equal(resp.data.urlId, 'puzzler');
// Try to make 'puzzler' a urlId for document within same org.
const did2 = await dbManager.testGetId('Apathy');
resp = await axios.patch(`${homeUrl}/api/docs/${did2}`, {urlId: 'puzzler'}, chimpy);
// Not allowed, since there's a live doc in the org using this urlId.
assert.equal(resp.status, 400);
// Remove the urlId from first doc
resp = await axios.patch(`${homeUrl}/api/docs/${did}`, {urlId: null}, chimpy);
assert.equal(resp.status, 200);
// The urlId should still forward (until we reuse it later).
resp = await axios.get(`${homeUrl}/api/docs/puzzler`, chimpy);
assert.equal(resp.status, 200);
assert.equal(resp.data.id, did);
// Try to make 'puzzler' a urlId for second document again, it should work this time.
resp = await axios.patch(`${homeUrl}/api/docs/${did2}`, {urlId: 'puzzler'}, chimpy);
assert.equal(resp.status, 200);
// Check we can now access new doc via urlId.
resp = await axios.get(`${homeUrl}/api/docs/puzzler`, chimpy);
assert.equal(resp.status, 200);
assert.equal(resp.data.id, did2);
assert.equal(resp.data.urlId, 'puzzler');
// Check that the first doc is accessible via its docId.
resp = await axios.get(`${homeUrl}/api/docs/${did}`, chimpy);
assert.equal(resp.status, 200);
assert.equal(resp.data.id, did);
assert.equal(resp.data.urlId, null);
});
it('PATCH /api/docs/{did} forbids funky urlIds', async function() {
const badUrlIds = new Set(['sp/ace', 'sp ace', 'space!', 'spa.ce', '']);
const goodUrlIds = new Set(['sp-ace', 'spAace', 'SPac3', 's']);
const did = await dbManager.testGetId('Curiosity');
let resp;
for (const urlId of [...badUrlIds, ...goodUrlIds]) {
resp = await axios.patch(`${homeUrl}/api/docs/${did}`, {urlId}, chimpy);
assert.equal(resp.status, goodUrlIds.has(urlId) ? 200 : 400);
}
for (const urlId of [...badUrlIds, ...goodUrlIds]) {
resp = await axios.get(`${homeUrl}/api/docs/${urlId}`, chimpy);
assert.equal(resp.status, goodUrlIds.has(urlId) ? 200 : 404);
}
// It is permissible to reset urlId to null
resp = await axios.patch(`${homeUrl}/api/docs/${did}`, {urlId: null}, chimpy);
assert.equal(resp.status, 200);
resp = await axios.get(`${homeUrl}/api/docs/sp-ace`, chimpy);
assert.equal(resp.status, 200);
assert.equal(resp.data.urlId, null);
});
it('PATCH /api/docs/{did} supports options', async function() {
// Set some options on the 'Surprise2' doc.
const did = await dbManager.testGetId('Surprise2');
let resp = await axios.patch(`${homeUrl}/api/docs/${did}`, {
options: { description: 'boo', openMode: 'fork' }
}, chimpy);
assert.equal(resp.status, 200);
// Check they show up in a workspace request.
const wid = await dbManager.testGetId('Rovers');
resp = await axios.get(`${homeUrl}/api/workspaces/${wid}`, chimpy);
const workspace: Workspace = resp.data;
const doc = workspace.docs.find(d => d.name === 'Surprise2');
assert.deepEqual(doc?.options, {description: 'boo', openMode: 'fork'});
// Check setting one option preserves others.
resp = await axios.patch(`${homeUrl}/api/docs/${did}`, {
options: { description: 'boo!' }
}, chimpy);
assert.equal(resp.status, 200);
resp = await axios.get(`${homeUrl}/api/docs/${did}`, chimpy);
assert.deepEqual(resp.data?.options, {description: 'boo!', openMode: 'fork'});
// Check setting to null removes an option.
resp = await axios.patch(`${homeUrl}/api/docs/${did}`, {
options: { openMode: null }
}, chimpy);
assert.equal(resp.status, 200);
resp = await axios.get(`${homeUrl}/api/docs/${did}`, chimpy);
assert.deepEqual(resp.data?.options, {description: 'boo!'});
// Check setting options object to null wipes it completely.
resp = await axios.patch(`${homeUrl}/api/docs/${did}`, {
options: null
}, chimpy);
assert.equal(resp.status, 200);
resp = await axios.get(`${homeUrl}/api/docs/${did}`, chimpy);
assert.deepEqual(resp.data?.options, undefined);
// Check setting icon works.
resp = await axios.patch(`${homeUrl}/api/docs/${did}`, {
options: { icon: 'https://grist-static.com/icons/foo.png' }
}, chimpy);
assert.equal(resp.status, 200);
resp = await axios.get(`${homeUrl}/api/docs/${did}`, chimpy);
assert.deepEqual(resp.data?.options, {icon: 'https://grist-static.com/icons/foo.png'});
// Check random urls are not supported.
resp = await axios.patch(`${homeUrl}/api/docs/${did}`, {
options: { icon: 'https://not-grist-static.com/icons/evil.exe' }
}, chimpy);
assert.equal(resp.status, 400);
resp = await axios.get(`${homeUrl}/api/docs/${did}`, chimpy);
assert.deepEqual(resp.data?.options, {icon: 'https://grist-static.com/icons/foo.png'});
// Check removing icon works.
resp = await axios.patch(`${homeUrl}/api/docs/${did}`, {
options: { icon: null },
}, chimpy);
assert.equal(resp.status, 200);
resp = await axios.get(`${homeUrl}/api/docs/${did}`, chimpy);
assert.deepEqual(resp.data?.options, undefined);
});
it('PATCH /api/docs/{did} returns 404 appropriately', async function() {
// Attempt to rename a doc that doesn't exist.
const resp = await axios.patch(`${homeUrl}/api/docs/9999`, {
name: 'Rename'
}, chimpy);
assert.equal(resp.status, 404);
});
it('PATCH /api/docs/{did} returns 403 with view access', async function() {
// Attempt to rename a doc without UPDATE access.
const did = await dbManager.testGetId('Bananas');
const resp = await axios.patch(`${homeUrl}/api/docs/${did}`, {
name: 'Bananas2'
}, chimpy);
assert.equal(resp.status, 403);
});
it('PATCH /api/docs/{did} returns 400 appropriately', async function() {
// Use an unavailable property and check that the operation fails with 400.
const did = await dbManager.testGetId('Surprise2');
const resp = await axios.patch(`${homeUrl}/api/docs/${did}`, {x: 1}, chimpy);
assert.equal(resp.status, 400);
});
it('DELETE /api/docs/{did} is operational', async function() {
const oid = await dbManager.testGetId('NASA');
const wid = await dbManager.testGetId('Rovers');
const did = await dbManager.testGetId('Surprise2');
// Assert that the number of users in the org has not been updated.
assert.deepEqual(userCountUpdates[oid as number], undefined);
// Add Kiwi to the 'Surprise2' doc.
const delta = {
users: {[kiwiEmail]: 'viewers'}
};
const accessResp = await axios.patch(`${homeUrl}/api/docs/${did}/access`, {delta}, chimpy);
assert.equal(accessResp.status, 200);
// Assert that Kiwi is a guest of the ws.
const wsResp = await axios.get(`${homeUrl}/api/workspaces/${wid}/access`, chimpy);
assert.equal(wsResp.status, 200);
assert.deepEqual(wsResp.data, {
maxInheritedRole: "owners",
users: [{
id: 1,
name: 'Chimpy',
email: chimpyEmail,
ref: chimpyRef,
picture: null,
access: 'guests',
parentAccess: "owners",
isMember: true,
}, {
id: 2,
name: 'Kiwi',
email: kiwiEmail,
ref: kiwiRef,
picture: null,
access: "guests",
parentAccess: null,
isMember: false,
}, {
// Note that Charon is listed despite lacking access since Charon is a guest of the org.
id: 3,
name: 'Charon',
email: charonEmail,
ref: charonRef,
picture: null,
access: null,
parentAccess: null,
isMember: false,
}]
});
// Assert that the number of non-guest users in the org is unchanged.
assert.deepEqual(userCountUpdates[oid as number], undefined);
// Assert that Kiwi is a guest of the org.
const orgResp = await axios.get(`${homeUrl}/api/orgs/${oid}/access`, chimpy);
assert.equal(orgResp.status, 200);
assert.deepEqual(orgResp.data, {
users: [{
id: 1,
name: 'Chimpy',
email: chimpyEmail,
ref: chimpyRef,
picture: null,
access: "owners",
isMember: true,
}, {
id: 2,
name: 'Kiwi',
email: kiwiEmail,
ref: kiwiRef,
picture: null,
access: "guests",
isMember: false,
}, {
id: 3,
name: 'Charon',
email: charonEmail,
ref: charonRef,
picture: null,
access: "guests",
isMember: false,
}]
});
const beforeDelCount = await getRowCounts();
// Delete the 'Surprise2' doc.
const deleteResp = await axios.delete(`${homeUrl}/api/docs/${did}`, chimpy);
// Assert that the response is successful.
assert.equal(deleteResp.status, 200);
// Assert that the doc is no longer in the database.
const fetchResp = await axios.get(`${homeUrl}/api/workspaces/${wid}`, chimpy);
const workspace = fetchResp.data;
assert.deepEqual(workspace.name, 'Rovers');
assert.deepEqual(workspace.docs.map((d: any) => d.name),
['Curiosity', 'Apathy', 'Boredom']);
// Assert that Kiwi is no longer a guest of the ws.
const wsResp2 = await axios.get(`${homeUrl}/api/workspaces/${wid}/access`, chimpy);
assert.equal(wsResp2.status, 200);
assert.deepEqual(wsResp2.data, {
maxInheritedRole: "owners",
users: [{
id: 1,
name: 'Chimpy',
email: chimpyEmail,
ref: chimpyRef,
picture: null,
access: 'guests',
parentAccess: "owners",
isMember: true,
}, {
// Note that Charon is listed despite lacking access since Charon is a guest of the org.
id: 3,
email: charonEmail,
ref: charonRef,
name: "Charon",
picture: null,
access: null,
parentAccess: null,
isMember: false,
}]
});
// Assert that Kiwi is no longer a guest of the org.
const orgResp2 = await axios.get(`${homeUrl}/api/orgs/${oid}/access`, chimpy);
assert.equal(orgResp2.status, 200);
assert.deepEqual(orgResp2.data, {
users: [{
id: 1,
name: 'Chimpy',
email: chimpyEmail,
ref: chimpyRef,
picture: null,
access: "owners",
isMember: true,
}, {
id: 3,
name: 'Charon',
email: charonEmail,
ref: charonRef,
picture: null,
access: "guests",
isMember: false,
}]
});
// Assert that the number of non-guest users in the org is unchanged.
assert.deepEqual(userCountUpdates[oid as number], undefined);
const afterDelCount = await getRowCounts();
// Assert that the doc was removed.
assert.equal(afterDelCount.docs, beforeDelCount.docs - 1);
// Assert that Chimpy doc owner item, Kiwi's doc viewer item, workspace guest and org guest items were removed.
assert.equal(afterDelCount.groupUsers, beforeDelCount.groupUsers - 4);
});
it('DELETE /api/docs/{did} returns 404 appropriately', async function() {
// Attempt to delete a doc that doesn't exist.
const resp = await axios.delete(`${homeUrl}/api/docs/9999`, chimpy);
assert.equal(resp.status, 404);
});
it('DELETE /api/docs/{did} returns 403 with view access', async function() {
// Attempt to delete a doc without REMOVE access.
const did = await dbManager.testGetId('Bananas');
const resp = await axios.delete(`${homeUrl}/api/docs/${did}`, chimpy);
assert.equal(resp.status, 403);
});
it('GET /api/zig is a 404', async function() {
const resp = await axios.get(`${homeUrl}/api/zig`, chimpy);
assert.equal(resp.status, 404);
assert.deepEqual(resp.data, {error: "not found: /api/zig"});
});
it('PATCH /api/docs/{did}/move is operational within the same org', async function() {
const did = await dbManager.testGetId('Jupiter');
const wsId1 = await dbManager.testGetId('Horizon');
const wsId2 = await dbManager.testGetId('Rovers');
const orgId = await dbManager.testGetId('NASA');
// Check that move returns 200
const resp1 = await axios.patch(`${homeUrl}/api/docs/${did}/move`,
{workspace: wsId2}, chimpy);
assert.equal(resp1.status, 200);
// Check that the doc is removed from the source workspace
const verifyResp1 = await axios.get(`${homeUrl}/api/workspaces/${wsId1}`, chimpy);
assert.deepEqual(verifyResp1.data.docs.map((doc: any) => doc.name), ['Pluto', 'Beyond']);
// Check that the doc is added to the dest workspace
const verifyResp2 = await axios.get(`${homeUrl}/api/workspaces/${wsId2}`, chimpy);
assert.deepEqual(verifyResp2.data.docs.map((doc: any) => doc.name),
['Jupiter', 'Curiosity', 'Apathy', 'Boredom']);
// Try a complex case - give a user special access to the doc then move it back
// Make Kiwi a doc editor for Jupiter
const delta1 = {
users: {[kiwiEmail]: 'editors'}
};
const accessResp1 = await axios.patch(`${homeUrl}/api/docs/${did}/access`, {delta: delta1}, chimpy);
assert.equal(accessResp1.status, 200);
// Check that Kiwi is a guest of the workspace/org
const kiwiResp1 = await axios.get(`${homeUrl}/api/workspaces/${wsId2}`, kiwi);
assert.equal(kiwiResp1.status, 200);
const kiwiResp2 = await axios.get(`${homeUrl}/api/orgs/${orgId}`, kiwi);
assert.equal(kiwiResp2.status, 200);
// Assert that the number of non-guest users in the org is unchanged.
assert.deepEqual(userCountUpdates[orgId as number], undefined);
// Move the doc back to Horizon
const resp2 = await axios.patch(`${homeUrl}/api/docs/${did}/move`,
{workspace: wsId1}, chimpy);
assert.equal(resp2.status, 200);
// Assert that the number of non-guest users in the org is unchanged.
assert.deepEqual(userCountUpdates[orgId as number], undefined);
// Check that the doc is removed from the source workspace
const verifyResp3 = await axios.get(`${homeUrl}/api/workspaces/${wsId2}`, chimpy);
assert.deepEqual(verifyResp3.data.docs.map((doc: any) => doc.name),
['Curiosity', 'Apathy', 'Boredom']);
// Check that the doc is added to the dest workspace
const verifyResp4 = await axios.get(`${homeUrl}/api/workspaces/${wsId1}`, chimpy);
assert.deepEqual(verifyResp4.data.docs.map((doc: any) => doc.name),
['Jupiter', 'Pluto', 'Beyond']);
// Check that Kiwi is NO LONGER a guest of the source workspace
const kiwiResp3 = await axios.get(`${homeUrl}/api/workspaces/${wsId2}`, kiwi);
assert.equal(kiwiResp3.status, 403);
// Check that Kiwi is a guest of the destination workspace
const kiwiResp5 = await axios.get(`${homeUrl}/api/workspaces/${wsId1}`, kiwi);
assert.equal(kiwiResp5.status, 200);
// Finish by revoking Kiwi's access
const delta2 = {
users: {[kiwiEmail]: null}
};
const accessResp2 = await axios.patch(`${homeUrl}/api/docs/${did}/access`, {delta: delta2}, chimpy);
assert.equal(accessResp2.status, 200);
// Assert that the number of non-guest users in the org is unchanged.
assert.deepEqual(userCountUpdates[orgId as number], undefined);
// Test adding a doc and moving it to a workspace with less access
const fishOrg = await dbManager.testGetId('Fish');
const bigWs = await dbManager.testGetId('Big');
const resp = await axios.post(`${homeUrl}/api/workspaces/${bigWs}/docs`, {
name: 'Magic'
}, chimpy);
// Assert that the response is successful and contains the doc id.
assert.equal(resp.status, 200);
const magicDocId = resp.data;
// Remove chimpy's direct owner permission on this document. Chimpy is added directly as an owner.
// We need to do it as a different user.
assert.equal((await axios.patch(`${homeUrl}/api/docs/${magicDocId}/access`, {delta: {
users: {[charonEmail]: 'owners'}
}}, chimpy)).status, 200);
assert.equal((await axios.patch(`${homeUrl}/api/docs/${magicDocId}/access`, {delta: {
users: {[chimpyEmail]: null}
}}, charon)).status, 200);
// Create a workspace and limit Chimpy's access to that workspace.
const addMediumWsResp = await axios.post(`${homeUrl}/api/orgs/${fishOrg}/workspaces`, {
name: 'Medium'
}, chimpy);
assert.equal(addMediumWsResp.status, 200);
const mediumWs = addMediumWsResp.data;
// Limit all access to it expect for Kiwi.
const delta3 = {
maxInheritedRole: null,
users: {[kiwiEmail]: 'owners'}
};
const accessResp3 = await axios.patch(`${homeUrl}/api/workspaces/${mediumWs}/access`,
{delta: delta3}, chimpy);
assert.equal(accessResp3.status, 200);
// Chimpy's access must be removed by Kiwi, since Chimpy would have been granted access
// by being unable to limit his own access.
const delta4 = {
users: {[chimpyEmail]: 'editors'}
};
const accessResp4 = await axios.patch(`${homeUrl}/api/workspaces/${mediumWs}/access`,
{delta: delta4}, kiwi);
assert.equal(accessResp4.status, 200);
// Move the doc to the new 'Medium' workspace.
const moveMagicResp = await axios.patch(`${homeUrl}/api/docs/${magicDocId}/move`,
{workspace: mediumWs}, chimpy);
assert.equal(moveMagicResp.status, 200);
// Check that doc access on magic can no longer be edited by chimpy
const delta = {
users: {
[kiwiEmail]: 'editors'
}
};
const accessResp = await axios.patch(`${homeUrl}/api/docs/${magicDocId}/access`,
{delta}, chimpy);
assert.equal(accessResp.status, 403);
// Finish by removing the added workspace
const removeResp = await axios.delete(`${homeUrl}/api/workspaces/${mediumWs}`, chimpy);
// Assert that the response is successful.
assert.equal(removeResp.status, 200);
});
it('PATCH /api/docs/{did}/move is operational between orgs', async function() {
const did = await dbManager.testGetId('Jupiter');
const srcWsId = await dbManager.testGetId('Horizon');
const srcOrgId = await dbManager.testGetId('NASA');
const dstWsId = await dbManager.testGetId('Private');
const dstOrgId = await dbManager.testGetId('Chimpyland');
// Check that move returns 200
const resp1 = await axios.patch(`${homeUrl}/api/docs/${did}/move`,
{workspace: dstWsId}, chimpy);
assert.equal(resp1.status, 200);
// Check that the doc is removed from the source workspace
const verifyResp1 = await axios.get(`${homeUrl}/api/workspaces/${srcWsId}`, chimpy);
assert.deepEqual(verifyResp1.data.docs.map((doc: any) => doc.name), ['Pluto', 'Beyond']);
// Check that the doc is added to the dest workspace
const verifyResp2 = await axios.get(`${homeUrl}/api/workspaces/${dstWsId}`, chimpy);
assert.deepEqual(verifyResp2.data.docs.map((doc: any) => doc.name),
['Jupiter', 'Timesheets', 'Appointments']);
// Assert that the number of non-guest users in the org is unchanged.
assert.deepEqual(userCountUpdates[srcOrgId as number], undefined);
assert.deepEqual(userCountUpdates[dstOrgId as number], undefined);
// Try a complex case - give a user special access to the doc then move it back
// Make Kiwi a doc editor for Jupiter
const delta1 = {
users: {[kiwiEmail]: 'editors'}
};
const accessResp1 = await axios.patch(`${homeUrl}/api/docs/${did}/access`, {delta: delta1}, chimpy);
assert.equal(accessResp1.status, 200);
// Check that Kiwi is a guest of the workspace/org
const kiwiResp1 = await axios.get(`${homeUrl}/api/workspaces/${dstWsId}`, kiwi);
assert.equal(kiwiResp1.status, 200);
const kiwiResp2 = await axios.get(`${homeUrl}/api/orgs/${dstOrgId}`, kiwi);
assert.equal(kiwiResp2.status, 200);
// Assert that the number of non-guest users in 'Chimpyland' is unchanged.
assert.deepEqual(userCountUpdates[srcOrgId as number], undefined);
assert.deepEqual(userCountUpdates[dstOrgId as number], undefined);
// Move the doc back to Horizon
const resp2 = await axios.patch(`${homeUrl}/api/docs/${did}/move`,
{workspace: srcWsId}, chimpy);
assert.equal(resp2.status, 200);
// Check that the doc is removed from the source workspace
const verifyResp3 = await axios.get(`${homeUrl}/api/workspaces/${dstWsId}`, chimpy);
assert.deepEqual(verifyResp3.data.docs.map((doc: any) => doc.name),
['Timesheets', 'Appointments']);
// Check that the doc is added to the dest workspace
const verifyResp4 = await axios.get(`${homeUrl}/api/workspaces/${srcWsId}`, chimpy);
assert.deepEqual(verifyResp4.data.docs.map((doc: any) => doc.name),
['Jupiter', 'Pluto', 'Beyond']);
// Check that Kiwi is NO LONGER a guest of the workspace/org
const kiwiResp3 = await axios.get(`${homeUrl}/api/workspaces/${dstWsId}`, kiwi);
assert.equal(kiwiResp3.status, 403);
const kiwiResp4 = await axios.get(`${homeUrl}/api/orgs/${dstOrgId}`, kiwi);
assert.equal(kiwiResp4.status, 403);
// Check that Kiwi is a guest of the new workspace/org
const kiwiResp5 = await axios.get(`${homeUrl}/api/workspaces/${srcWsId}`, kiwi);
assert.equal(kiwiResp5.status, 200);
const kiwiResp6 = await axios.get(`${homeUrl}/api/orgs/${srcOrgId}`, kiwi);
assert.equal(kiwiResp6.status, 200);
// Assert that the number of non-guest users in the orgs have not changed.
assert.deepEqual(userCountUpdates[srcOrgId as number], undefined);
assert.deepEqual(userCountUpdates[dstOrgId as number], undefined);
// Finish by revoking Kiwi's access
const delta2 = {
users: {[kiwiEmail]: null}
};
const accessResp2 = await axios.patch(`${homeUrl}/api/docs/${did}/access`, {delta: delta2}, chimpy);
assert.equal(accessResp2.status, 200);
// Assert that the number of non-guest users in the orgs have not changed.
assert.deepEqual(userCountUpdates[srcOrgId as number], undefined);
assert.deepEqual(userCountUpdates[dstOrgId as number], undefined);
// Add a doc and move it to a workspace with less access
const publicWs = await dbManager.testGetId('Public');
const resp = await axios.post(`${homeUrl}/api/workspaces/${publicWs}/docs`, {
name: 'Magic'
}, chimpy);
// Assert that the response is successful and contains the doc id.
assert.equal(resp.status, 200);
const magicDocId = resp.data;
// Remove chimpy's direct owner permission on this document. Chimpy is added directly as an owner.
// We need to do it as a different user.
assert.equal((await axios.patch(`${homeUrl}/api/docs/${magicDocId}/access`, {delta: {
users: {[charonEmail]: 'owners'}
}}, chimpy)).status, 200);
assert.equal((await axios.patch(`${homeUrl}/api/docs/${magicDocId}/access`, {delta: {
users: {[chimpyEmail]: null}
}}, charon)).status, 200);
// Move the doc to Vacuum
const vacuum = await dbManager.testGetId('Vacuum');
const moveMagicResp = await axios.patch(`${homeUrl}/api/docs/${magicDocId}/move`,
{workspace: vacuum}, chimpy);
assert.equal(moveMagicResp.status, 200);
// Check that doc access on magic can no longer be edited by chimpy
const delta = {
users: {
[kiwiEmail]: 'editors'
}
};
const accessResp = await axios.patch(`${homeUrl}/api/docs/${magicDocId}/access`,
{delta}, chimpy);
assert.equal(accessResp.status, 403);
// Finish by removing the added doc
let removeResp = await axios.delete(`${homeUrl}/api/docs/${magicDocId}`, chimpy);
// Assert that the response is a failure - we are only editors.
assert.equal(removeResp.status, 403);
const store = server.getWorkStore().getPermitStore('internal');
const goodDocPermit = await store.setPermit({docId: magicDocId});
removeResp = await axios.delete(`${homeUrl}/api/docs/${magicDocId}`, configWithPermit(chimpy, goodDocPermit));
assert.equal(removeResp.status, 200);
});
it('PATCH /api/docs/{did}/move returns 404 appropriately', async function() {
const workspace = await dbManager.testGetId('Private');
const resp = await axios.patch(`${homeUrl}/api/docs/9999/move`, {workspace}, chimpy);
assert.equal(resp.status, 404);
});
it('PATCH /api/docs/{did}/move returns 403 appropriately', async function() {
// Attempt moving a doc that the caller does not own, assert that it fails.
const did = await dbManager.testGetId('Bananas');
const workspace = await dbManager.testGetId('Private');
const resp = await axios.patch(`${homeUrl}/api/docs/${did}/move`, {workspace}, chimpy);
assert.equal(resp.status, 403);
// Attempt moving a doc that the caller owns to a workspace to which they do not
// have ADD access. Assert that it fails.
const did2 = await dbManager.testGetId('Timesheets');
const workspace2 = await dbManager.testGetId('Fruit');
const resp2 = await axios.patch(`${homeUrl}/api/docs/${did2}/move`,
{workspace: workspace2}, chimpy);
assert.equal(resp2.status, 403);
});
it('PATCH /api/docs/{did}/move returns 400 appropriately', async function() {
// Assert that attempting to move a doc to the workspace it starts in
// returns 400
const did = await dbManager.testGetId('Jupiter');
const srcWsId = await dbManager.testGetId('Horizon');
const resp = await axios.patch(`${homeUrl}/api/docs/${did}/move`,
{workspace: srcWsId}, chimpy);
assert.equal(resp.status, 400);
});
it('PATCH /api/docs/:did/pin is operational', async function() {
const nasaOrgId = await dbManager.testGetId('NASA');
const chimpylandOrgId = await dbManager.testGetId('Chimpyland');
const plutoDocId = await dbManager.testGetId('Pluto');
const timesheetsDocId = await dbManager.testGetId('Timesheets');
const appointmentsDocId = await dbManager.testGetId('Appointments');
// Pin 3 docs in 2 different orgs.
const resp1 = await axios.patch(`${homeUrl}/api/docs/${plutoDocId}/pin`, {}, chimpy);
assert.equal(resp1.status, 200);
const resp2 = await axios.patch(`${homeUrl}/api/docs/${timesheetsDocId}/pin`, {}, chimpy);
assert.equal(resp2.status, 200);
const resp3 = await axios.patch(`${homeUrl}/api/docs/${appointmentsDocId}/pin`, {}, chimpy);
assert.equal(resp3.status, 200);
// Assert that the docs are set as pinned when retrieved.
const fetchResp1 = await axios.get(`${homeUrl}/api/orgs/${nasaOrgId}/workspaces`, charon);
assert.equal(fetchResp1.status, 200);
assert.deepEqual(fetchResp1.data[0].docs.map((doc: any) => omit(doc, 'createdAt', 'updatedAt')), [{
id: await dbManager.testGetId('Pluto'),
name: 'Pluto',
access: 'viewers',
isPinned: true,
urlId: null,
trunkId: null,
type: null,
forks: [],
}]);
const fetchResp2 = await axios.get(`${homeUrl}/api/orgs/${chimpylandOrgId}/workspaces`, charon);
assert.equal(fetchResp2.status, 200);
const privateWs = fetchResp2.data.find((ws: any) => ws.name === 'Private');
assert.deepEqual(privateWs.docs.map((doc: any) => omit(doc, 'createdAt', 'updatedAt')), [{
id: timesheetsDocId,
name: 'Timesheets',
access: 'viewers',
isPinned: true,
urlId: null,
trunkId: null,
type: null,
forks: [],
}, {
id: appointmentsDocId,
name: 'Appointments',
access: 'viewers',
isPinned: true,
urlId: null,
trunkId: null,
type: null,
forks: [],
}]);
// Pin a doc that is already pinned and assert that it returns 200.
const resp4 = await axios.patch(`${homeUrl}/api/docs/${appointmentsDocId}/pin`, {}, chimpy);
assert.equal(resp4.status, 200);
});
it('PATCH /api/docs/:did/pin returns 404 appropriately', async function() {
// Attempt to pin a doc that doesn't exist.
const resp1 = await axios.patch(`${homeUrl}/api/docs/9999/pin`, {}, charon);
assert.equal(resp1.status, 404);
});
it('PATCH /api/docs/:did/pin returns 403 appropriately', async function() {
const antarticDocId = await dbManager.testGetId('Antartic');
const sharkDocId = await dbManager.testGetId('Shark');
// Attempt to pin a doc with only view access (should fail).
const resp1 = await axios.patch(`${homeUrl}/api/docs/${antarticDocId}/pin`, {}, chimpy);
assert.equal(resp1.status, 403);
// Attempt to pin a doc with org edit access but no doc access (should succeed).
const delta1 = { maxInheritedRole: 'viewers' };
const setupResp1 = await axios.patch(`${homeUrl}/api/docs/${sharkDocId}/access`, {delta: delta1}, chimpy);
assert.equal(setupResp1.status, 200);
// Check that access to shark is as expected.
const setupResp2 = await axios.get(`${homeUrl}/api/docs/${sharkDocId}/access`, chimpy);
assert.equal(setupResp2.status, 200);
assert.deepEqual(setupResp2.data, {
maxInheritedRole: 'viewers',
users: [{
id: 1,
name: 'Chimpy',
email: chimpyEmail,
ref: chimpyRef,
picture: null,
access: "owners", // Chimpy's access is explicit since he is the owner who set access.
parentAccess: "owners",
isMember: true,
}, {
id: 2,
name: 'Kiwi',
email: kiwiEmail,
ref: kiwiRef,
picture: null,
access: null,
parentAccess: "editors",
isMember: true,
}, {
id: 3,
name: 'Charon',
email: charonEmail,
ref: charonRef,
picture: null,
access: null,
parentAccess: "viewers",
isMember: true,
}]
});
// Perform the pin.
const resp2 = await axios.patch(`${homeUrl}/api/docs/${sharkDocId}/pin`, {}, kiwi);
assert.equal(resp2.status, 200);
// And unpin and restore access to keep the state consistent.
const setupResp3 = await axios.patch(`${homeUrl}/api/docs/${sharkDocId}/unpin`, {}, kiwi);
assert.equal(setupResp3.status, 200);
// Attempt to pin a doc with viewer org access but edit doc access (should fail).
const delta2 = {
maxInheritedRole: 'owners',
users: {
[charonEmail]: 'editors'
}
};
const setupResp4 = await axios.patch(`${homeUrl}/api/docs/${sharkDocId}/access`, {delta: delta2}, chimpy);
assert.equal(setupResp4.status, 200);
// Check that access to shark is as expected.
const setupResp5 = await axios.get(`${homeUrl}/api/docs/${sharkDocId}/access`, chimpy);
assert.equal(setupResp5.status, 200);
assert.deepEqual(setupResp5.data, {
maxInheritedRole: 'owners',
users: [{
id: 1,
name: 'Chimpy',
email: chimpyEmail,
ref: chimpyRef,
picture: null,
access: 'owners',
parentAccess: 'owners',
isMember: true,
}, {
id: 2,
name: 'Kiwi',
email: kiwiEmail,
ref: kiwiRef,
picture: null,
access: null,
parentAccess: 'editors',
isMember: true,
}, {
id: 3,
name: 'Charon',
email: charonEmail,
ref: charonRef,
picture: null,
access: 'editors',
parentAccess: 'viewers',
isMember: true,
}]
});
// Attempt the pin.
const resp3 = await axios.patch(`${homeUrl}/api/docs/${sharkDocId}/pin`, {}, charon);
assert.equal(resp3.status, 403);
// Restore access to keep the state consistent.
const delta4 = {
users: {
[charonEmail]: null
}
};
const setupResp6 = await axios.patch(`${homeUrl}/api/docs/${sharkDocId}/access`, {delta: delta4}, chimpy);
assert.equal(setupResp6.status, 200);
});
it('PATCH /api/docs/:did/unpin is operational', async function() {
const chimpylandOrgId = await dbManager.testGetId('Chimpyland');
const plutoDocId = await dbManager.testGetId('Pluto');
const timesheetsDocId = await dbManager.testGetId('Timesheets');
const appointmentsDocId = await dbManager.testGetId('Appointments');
// Unpin 3 previously pinned docs.
const resp1 = await axios.patch(`${homeUrl}/api/docs/${plutoDocId}/unpin`, {}, chimpy);
assert.equal(resp1.status, 200);
const resp2 = await axios.patch(`${homeUrl}/api/docs/${timesheetsDocId}/unpin`, {}, chimpy);
assert.equal(resp2.status, 200);
const resp3 = await axios.patch(`${homeUrl}/api/docs/${appointmentsDocId}/unpin`, {}, chimpy);
assert.equal(resp3.status, 200);
// Fetch pinned docs to ensure the docs are no longer pinned.
const fetchResp1 = await axios.get(`${homeUrl}/api/orgs/${chimpylandOrgId}/workspaces`, charon);
assert.equal(fetchResp1.status, 200);
const privateWs = fetchResp1.data.find((ws: any) => ws.name === 'Private');
assert.deepEqual(privateWs.docs.map((doc: any) => omit(doc, 'createdAt', 'updatedAt')), [{
id: timesheetsDocId,
name: 'Timesheets',
access: 'viewers',
isPinned: false,
urlId: null,
trunkId: null,
type: null,
forks: [],
}, {
id: appointmentsDocId,
name: 'Appointments',
access: 'viewers',
isPinned: false,
urlId: null,
trunkId: null,
type: null,
forks: [],
}]);
// Unpin doc that is already not pinned and assert that it returns 200.
const resp4 = await axios.patch(`${homeUrl}/api/docs/${plutoDocId}/unpin`, {}, chimpy);
assert.equal(resp4.status, 200);
});
it('PATCH /api/docs/:did/unpin returns 404 appropriately', async function() {
// Attempt to unpin a doc that doesn't exist.
const resp1 = await axios.patch(`${homeUrl}/api/docs/9999/unpin`, {}, charon);
assert.equal(resp1.status, 404);
});
it('PATCH /api/docs/:did/unpin returns 403 appropriately', async function() {
const antarticDocId = await dbManager.testGetId('Antartic');
// Attempt to pin a doc with only view access (should fail).
const resp1 = await axios.patch(`${homeUrl}/api/docs/${antarticDocId}/pin`, {}, chimpy);
assert.equal(resp1.status, 403);
});
it('GET /api/profile/user returns user info', async function() {
const resp = await axios.get(`${homeUrl}/api/profile/user`, chimpy);
assert.equal(resp.status, 200);
assert.deepEqual(resp.data, {
id: 1,
email: chimpyEmail,
ref: chimpyRef,
name: "Chimpy",
picture: null,
allowGoogleLogin: true,
});
});
it('GET /api/profile/user can return anonymous user', async function() {
const resp = await axios.get(`${homeUrl}/api/profile/user`, nobody);
assert.equal(resp.status, 200);
assert.equal(resp.data.email, "anon@getgrist.com");
assert.equal(resp.data.anonymous, true);
});
it('POST /api/profile/user/name updates user\' name', async function() {
let resp: AxiosResponse<any>;
async function getName(config: AxiosRequestConfig = chimpy) {
return (await axios.get(`${homeUrl}/api/profile/user`, config)).data.name;
}
// name should 'Chimpy' initially
assert.equal(await getName(), 'Chimpy');
// let's change it
resp = await axios.post(`${homeUrl}/api/profile/user/name`, {name: 'babaganoush'}, chimpy);
assert.equal(resp.status, 200);
// check
assert.equal(await getName(), "babaganoush");
// revert to 'Chimpy'
resp = await axios.post(`${homeUrl}/api/profile/user/name`, {name: 'Chimpy'}, chimpy);
assert.equal(resp.status, 200);
assert.equal(await getName(), "Chimpy");
// requests treated as bad unless it has name provided
resp = await axios.post(`${homeUrl}/api/profile/user/name`, null, chimpy);
assert.equal(resp.status, 400);
assert.match(resp.data.error, /name expected/i);
// anonymous user not allowed to set name
resp = await axios.post(`${homeUrl}/api/profile/user/name`, {name: 'Testy'}, nobody);
assert.equal(resp.status, 401);
assert.match(resp.data.error, /not authorized/i);
assert.equal(await getName(nobody), "Anonymous");
});
it('POST /api/profile/allowGoogleLogin updates Google login preference', async function() {
let loginCookie: AxiosRequestConfig = await server.getCookieLogin('nasa', {
email: 'chimpy@getgrist.com',
name: 'Chimpy',
loginMethod: 'Email + Password',
});
async function isGoogleLoginAllowed() {
return (await axios.get(`${homeUrl}/api/profile/user`, loginCookie)).data.allowGoogleLogin;
}
// Should be set to true by default.
assert(await isGoogleLoginAllowed());
// Setting it via an email/password session should work.
let resp = await axios.post(`${homeUrl}/api/profile/allowGoogleLogin`, {allowGoogleLogin: false}, loginCookie);
assert.equal(resp.status, 200);
assert.equal(await isGoogleLoginAllowed(), false);
// Setting it without the body param should fail.
resp = await axios.post(`${homeUrl}/api/profile/allowGoogleLogin`, null, loginCookie);
assert.equal(resp.status, 400);
assert.equal(resp.data.error, 'Missing body param: allowGoogleLogin');
// Setting it via API or a Google login session should fail.
loginCookie = chimpy;
resp = await axios.post(`${homeUrl}/api/profile/allowGoogleLogin`, {allowGoogleLogin: false}, loginCookie);
assert.equal(resp.status, 401);
assert.equal(resp.data.error, 'Only users signed in via email can enable/disable Google login');
assert.equal(await isGoogleLoginAllowed(), false);
loginCookie = await server.getCookieLogin('nasa', {
email: 'chimpy@getgrist.com',
name: 'Chimpy',
loginMethod: 'Google',
});
resp = await axios.post(`${homeUrl}/api/profile/allowGoogleLogin`, {allowGoogleLogin: false}, loginCookie);
assert.equal(resp.status, 401);
assert.equal(resp.data.error, 'Only users signed in via email can enable/disable Google login');
assert.equal(await isGoogleLoginAllowed(), false);
// Setting it as an anonymous user should fail.
resp = await axios.post(`${homeUrl}/api/profile/allowGoogleLogin`, {allowGoogleLogin: false}, nobody);
assert.equal(resp.status, 401);
assert.match(resp.data.error, /not authorized/i);
});
it('DELETE /api/user/:uid can delete a user', async function() {
const countsBefore = await getRowCounts();
// create a new user
const profile = {email: 'meep@getgrist.com', name: 'Meep'};
const user = await dbManager.getUserByLogin('meep@getgrist.com', {profile});
assert(user);
const userId = user!.id;
// set up an api key
await dbManager.connection.query("update users set api_key = 'api_key_for_meep' where id = $1", [userId]);
// make sure we have at least one extra user, a login, an org, a group_user entry,
// a billing account, a billing account manager.
const countsWithMeep = await getRowCounts();
assert.isAbove(countsWithMeep.users, countsBefore.users);
assert.isAbove(countsWithMeep.logins, countsBefore.logins);
assert.isAbove(countsWithMeep.orgs, countsBefore.orgs);
assert.isAbove(countsWithMeep.groupUsers, countsBefore.groupUsers);
assert.isAbove(countsWithMeep.billingAccounts, countsBefore.billingAccounts);
assert.isAbove(countsWithMeep.billingAccountManagers, countsBefore.billingAccountManagers);
// requests treated as bad unless it has name provided
let resp = await axios.delete(`${homeUrl}/api/users/${userId}`, configForUser("meep"));
assert.equal(resp.status, 400);
assert.match(resp.data.error, /provide their name/);
// others cannot delete this user
resp = await axios.delete(`${homeUrl}/api/users/${userId}`,
{data: {name: "Meep"}, ...configForUser("chimpy")});
assert.equal(resp.status, 403);
assert.match(resp.data.error, /not permitted/);
// user cannot delete themselves if they get their name wrong
resp = await axios.delete(`${homeUrl}/api/users/${userId}`,
{data: {name: "Moop"}, ...configForUser("meep")});
assert.equal(resp.status, 400);
assert.match(resp.data.error, /user name did not match/);
// user can delete themselves if they get the name right
resp = await axios.delete(`${homeUrl}/api/users/${userId}`,
{data: {name: "Meep"}, ...configForUser("meep")});
assert.equal(resp.status, 200);
// create a user with a blank name
const userBlank = await dbManager.getUserByLogin('blank@getgrist.com',
{profile: {email: 'blank@getgrist.com',
name: ''}});
assert(userBlank);
await dbManager.connection.query("update users set api_key = 'api_key_for_blank' where id = $1", [userBlank!.id]);
// check that user can delete themselves
resp = await axios.delete(`${homeUrl}/api/users/${userBlank!.id}`,
{data: {name: ""}, ...configForUser("blank")});
assert.equal(resp.status, 200);
const countsAfter = await getRowCounts();
assert.deepEqual(countsAfter, countsBefore);
});
describe('GET /api/orgs/{oid}/usage', function() {
let freeTeamOrgId: number;
let freeTeamWorkspaceId: number;
async function assertOrgUsage(
orgId: string | number,
user: AxiosRequestConfig,
expected: OrgUsageSummary | 'denied'
) {
const resp = await axios.get(`${homeUrl}/api/orgs/${orgId}/usage`, user);
if (expected === 'denied') {
assert.equal(resp.status, 403);
assert.deepEqual(resp.data, {error: 'access denied'});
} else {
assert.equal(resp.status, 200);
assert.deepEqual(resp.data, expected);
}
}
before(async () => {
// Set up a free team site for testing usage. Avoid using billing endpoints,
// which may not be available in all test environments.
await axios.post(`${homeUrl}/api/orgs`, {
name: 'best-friends-squad',
domain: 'best-friends-squad',
}, chimpy);
freeTeamOrgId = await dbManager.testGetId('best-friends-squad') as number;
const prevAccount = await dbManager.getBillingAccount(
{userId: dbManager.getPreviewerUserId()},
'best-friends-squad', false);
await dbManager.connection.query(
'update billing_accounts set product_id = (select id from products where name = $1) where id = $2',
[TEAM_FREE_PLAN, prevAccount.id]
);
const resp = await axios.post(`${homeUrl}/api/orgs/${freeTeamOrgId}/workspaces`, {
name: 'TestUsage'
}, chimpy);
freeTeamWorkspaceId = resp.data;
});
after(async () => {
// Remove the free team site.
await axios.delete(`${homeUrl}/api/orgs/${freeTeamOrgId}`, chimpy);
});
it('is operational', async function() {
await assertOrgUsage(freeTeamOrgId, chimpy, createEmptyOrgUsageSummary());
const nasaOrgId = await dbManager.testGetId('NASA');
await assertOrgUsage(nasaOrgId, chimpy, createEmptyOrgUsageSummary());
});
it('requires owners access', async function() {
await assertOrgUsage(freeTeamOrgId, kiwi, 'denied');
const kiwilandOrgId = await dbManager.testGetId('Kiwiland');
await assertOrgUsage(kiwilandOrgId, kiwi, createEmptyOrgUsageSummary());
});
it('fails if user is anon', async function() {
await assertOrgUsage(freeTeamOrgId, nobody, 'denied');
const primatelyOrgId = await dbManager.testGetId('Primately');
await assertOrgUsage(primatelyOrgId, nobody, 'denied');
});
it('reports count of docs approaching/exceeding limits', async function() {
// Add a handful of documents to the TestUsage workspace.
const promises = [];
for (const name of ['GoodStanding', 'ApproachingLimits', 'GracePeriod', 'DeleteOnly']) {
promises.push(axios.post(`${homeUrl}/api/workspaces/${freeTeamWorkspaceId}/docs`, {name}, chimpy));
}
const docIds: string[] = (await Promise.all(promises)).map(resp => resp.data);
// Prepare one of each usage type.
const goodStanding = {rowCount: {total: 100}, dataSizeBytes: 1024, attachmentsSizeBytes: 4096};
const approachingLimits = {rowCount: {total: 4501}, dataSizeBytes: 4501 * 2 * 1024, attachmentsSizeBytes: 4096};
const gracePeriod = {rowCount: {total: 5001}, dataSizeBytes: 5001 * 2 * 1024, attachmentsSizeBytes: 4096};
const deleteOnly = gracePeriod;
// Set usage for each document. (This is normally done by ActiveDoc, but we
// facilitate here to keep the tests simple.)
const docUsage = [goodStanding, approachingLimits, gracePeriod, deleteOnly];
const idsAndUsage = docIds.map((id, i) => [id, docUsage[i]] as const);
for (const [id, usage] of idsAndUsage) {
await server.dbManager.setDocsMetadata({[id]: {usage}});
}
await server.dbManager.setDocGracePeriodStart(docIds[docIds.length - 1], new Date(2000, 1, 1));
// Check that what's reported by /usage is accurate.
await assertOrgUsage(freeTeamOrgId, chimpy, {
approachingLimit: 1,
gracePeriod: 1,
deleteOnly: 1,
});
});
it('only counts documents from org in path', async function() {
// Check NASA's usage once more, and make sure everything is still 0. This test is mostly
// a sanity check that results are in fact scoped by org.
const nasaOrgId = await dbManager.testGetId('NASA');
await assertOrgUsage(nasaOrgId, chimpy, createEmptyOrgUsageSummary());
});
it('excludes soft-deleted documents from count', async function() {
// Add another document that's exceeding limits.
const docId: string = (await axios.post(`${homeUrl}/api/workspaces/${freeTeamWorkspaceId}/docs`, {
name: 'SoftDeleted'
}, chimpy)).data;
await server.dbManager.setDocsMetadata({[docId]: {usage: {
rowCount: {total: 9999},
dataSizeBytes: 999999999,
attachmentsSizeBytes: 999999999,
}}});
// Check that /usage includes that document in the count.
await assertOrgUsage(freeTeamOrgId, chimpy, {
approachingLimit: 1,
gracePeriod: 2,
deleteOnly: 1,
});
// Now soft-delete the newly added document; make sure /usage no longer counts it.
await axios.post(`${homeUrl}/api/docs/${docId}/remove`, {}, chimpy);
await assertOrgUsage(freeTeamOrgId, chimpy, {
approachingLimit: 1,
gracePeriod: 1,
deleteOnly: 1,
});
});
it('excludes soft-deleted workspaces from count', async function() {
// Remove the workspace containing all docs.
await axios.post(`${homeUrl}/api/workspaces/${freeTeamWorkspaceId}/remove`, {}, chimpy);
// Check that /usage now reports a count of zero for all status types.
await assertOrgUsage(freeTeamOrgId, chimpy, createEmptyOrgUsageSummary());
});
});
// Template test moved to the end, since it deletes an org and makes
// predicting ids a little trickier.
it('GET /api/templates is operational', async function() {
let oid;
try {
// Add a 'Grist Templates' org.
await axios.post(`${homeUrl}/api/orgs`, {
name: 'Grist Templates',
domain: 'templates',
}, support);
oid = await dbManager.testGetId('Grist Templates');
// Add some workspaces and templates (documents) to Grist Templates.
const crmWsId = (await axios.post(`${homeUrl}/api/orgs/${oid}/workspaces`, {name: 'CRM'}, support)).data;
const invoiceWsId = (await axios.post(`${homeUrl}/api/orgs/${oid}/workspaces`, {name: 'Invoice'}, support)).data;
const crmDocId = (await axios.post(`${homeUrl}/api/workspaces/${crmWsId}/docs`,
{name: 'Lightweight CRM', isPinned: true}, support)).data;
const reportDocId = (await axios.post(`${homeUrl}/api/workspaces/${invoiceWsId}/docs`,
{name: 'Expense Report'}, support)).data;
const timesheetDocId = (await axios.post(`${homeUrl}/api/workspaces/${invoiceWsId}/docs`,
{name: 'Timesheet'}, support)).data;
// Make anon@/everyone@ a viewer of the public docs on Grist Templates.
for (const id of [crmDocId, reportDocId, timesheetDocId]) {
await axios.patch(`${homeUrl}/api/docs/${id}/access`, {
delta: {users: {'anon@getgrist.com': 'viewers', 'everyone@getgrist.com': 'viewers'}}
}, support);
}
// Make a request to retrieve all templates as an anonymous user.
const resp = await axios.get(`${homeUrl}/api/templates`, nobody);
// Assert that the response contains the right workspaces and template documents.
assert.equal(resp.status, 200);
assert.lengthOf(resp.data, 2);
assert.deepEqual(resp.data.map((ws: any) => ws.name), ['CRM', 'Invoice']);
assert.deepEqual(resp.data[0].docs.map((doc: any) => doc.name), ['Lightweight CRM']);
assert.deepEqual(resp.data[1].docs.map((doc: any) => doc.name), ['Expense Report', 'Timesheet']);
// Make a request to retrieve only the featured (pinned) templates.
const resp2 = await axios.get(`${homeUrl}/api/templates/?onlyFeatured=1`, nobody);
// Assert that the response includes only pinned documents.
assert.equal(resp2.status, 200);
assert.lengthOf(resp2.data, 2);
assert.deepEqual(resp.data.map((ws: any) => ws.name), ['CRM', 'Invoice']);
assert.deepEqual(resp2.data[0].docs.map((doc: any) => doc.name), ['Lightweight CRM']);
assert.deepEqual(resp2.data[1].docs, []);
// Add a new document to the CRM workspace, but don't share it with everyone.
await axios.post(`${homeUrl}/api/workspaces/${crmWsId}/docs`,
{name: 'Draft CRM Template', isPinned: true}, support);
// Make another request to retrieve all templates as an anonymous user.
const resp3 = await axios.get(`${homeUrl}/api/templates`, nobody);
// Assert that the response does not include the new document.
assert.lengthOf(resp3.data, 2);
assert.deepEqual(resp3.data[0].docs.map((doc: any) => doc.name), ['Lightweight CRM']);
} finally {
// Remove the 'Grist Templates' org.
if (oid) {
await axios.delete(`${homeUrl}/api/orgs/${oid}`, support);
}
}
});
it('GET /api/templates returns 404 appropriately', async function() {
// The 'Grist Templates' org currently doesn't exist.
const resp = await axios.get(`${homeUrl}/api/templates`, nobody);
// Assert that the response status is 404 because the templates org doesn't exist.
assert.equal(resp.status, 404);
});
});
// Predict the next id that will be used for a table.
// Only reliable if we haven't been deleting records in that table.
// Could make reliable by using sqlite_sequence in sqlite and the equivalent
// in postgres.
async function getNextId(dbManager: HomeDBManager, table: 'orgs'|'workspaces') {
// Check current top org id.
const row = await dbManager.connection.query(`select max(id) as id from ${table}`);
const id = row[0]['id'];
return id + 1;
}