gristlabs_grist-core/test/gen-server/ApiServer.ts
George Gevoian da6c39aa50 (core) Add new home page cards
Summary:
New cards on the home page link to useful resources like the welcome
video, tutorial, webinars, and the Help Center. They are shown by
default to new and exisiting users, and may be hidden via a toggle.

Test Plan: Browser tests.

Reviewers: jarek

Reviewed By: jarek

Differential Revision: https://phab.getgrist.com/D4340
2024-09-17 17:32:10 -04:00

2339 lines
95 KiB
TypeScript

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/homedb/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});
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: ''}});
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));
await server.dbManager.setDocGracePeriodStart(docIds[docIds.length - 2], new Date());
// 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,
}}});
await server.dbManager.setDocGracePeriodStart(docId, new Date());
// 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']);
// 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;
}