mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
208 lines
7.5 KiB
TypeScript
208 lines
7.5 KiB
TypeScript
|
import { TelemetryEvent, TelemetryMetadataByLevel } from 'app/common/Telemetry';
|
||
|
import { Document } from 'app/gen-server/entity/Document';
|
||
|
import { Workspace } from 'app/gen-server/entity/Workspace';
|
||
|
import { Housekeeper } from 'app/gen-server/lib/Housekeeper';
|
||
|
import { Telemetry } from 'app/server/lib/Telemetry';
|
||
|
import { assert } from 'chai';
|
||
|
import * as fse from 'fs-extra';
|
||
|
import moment from 'moment';
|
||
|
import * as sinon from 'sinon';
|
||
|
import { TestServer } from 'test/gen-server/apiUtils';
|
||
|
import { openClient } from 'test/server/gristClient';
|
||
|
import * as testUtils from 'test/server/testUtils';
|
||
|
|
||
|
describe('Housekeeper', function() {
|
||
|
testUtils.setTmpLogLevel('error');
|
||
|
this.timeout(60000);
|
||
|
|
||
|
const org: string = 'testy';
|
||
|
const sandbox = sinon.createSandbox();
|
||
|
let home: TestServer;
|
||
|
let keeper: Housekeeper;
|
||
|
|
||
|
before(async function() {
|
||
|
home = new TestServer(this);
|
||
|
await home.start(['home', 'docs']);
|
||
|
const api = await home.createHomeApi('chimpy', 'docs');
|
||
|
await api.newOrg({name: org, domain: org});
|
||
|
keeper = home.server.housekeeper;
|
||
|
await keeper.stop();
|
||
|
});
|
||
|
|
||
|
after(async function() {
|
||
|
await home.stop();
|
||
|
sandbox.restore();
|
||
|
});
|
||
|
|
||
|
async function getDoc(docId: string) {
|
||
|
const manager = home.dbManager.connection.manager;
|
||
|
return manager.findOneOrFail(Document, {where: {id: docId}});
|
||
|
}
|
||
|
|
||
|
async function getWorkspace(wsId: number) {
|
||
|
const manager = home.dbManager.connection.manager;
|
||
|
return manager.findOneOrFail(Workspace, {where: {id: wsId}});
|
||
|
}
|
||
|
|
||
|
|
||
|
function daysAgo(days: number): Date {
|
||
|
return moment().subtract(days, 'days').toDate();
|
||
|
}
|
||
|
|
||
|
async function ageDoc(docId: string, days: number) {
|
||
|
const dbDoc = await getDoc(docId);
|
||
|
dbDoc.removedAt = daysAgo(days);
|
||
|
await dbDoc.save();
|
||
|
}
|
||
|
|
||
|
async function ageWorkspace(wsId: number, days: number) {
|
||
|
const dbWorkspace = await getWorkspace(wsId);
|
||
|
dbWorkspace.removedAt = daysAgo(days);
|
||
|
await dbWorkspace.save();
|
||
|
}
|
||
|
|
||
|
async function ageFork(forkId: string, days: number) {
|
||
|
const dbFork = await getDoc(forkId);
|
||
|
dbFork.updatedAt = daysAgo(days);
|
||
|
await dbFork.save();
|
||
|
}
|
||
|
|
||
|
it('can delete old soft-deleted docs and workspaces', async function() {
|
||
|
// Make four docs in one workspace, two in another.
|
||
|
const api = await home.createHomeApi('chimpy', org);
|
||
|
const ws1 = await api.newWorkspace({name: 'ws1'}, 'current');
|
||
|
const ws2 = await api.newWorkspace({name: 'ws2'}, 'current');
|
||
|
const doc11 = await api.newDoc({name: 'doc11'}, ws1);
|
||
|
const doc12 = await api.newDoc({name: 'doc12'}, ws1);
|
||
|
const doc13 = await api.newDoc({name: 'doc13'}, ws1);
|
||
|
const doc14 = await api.newDoc({name: 'doc14'}, ws1);
|
||
|
const doc21 = await api.newDoc({name: 'doc21'}, ws2);
|
||
|
const doc22 = await api.newDoc({name: 'doc22'}, ws2);
|
||
|
|
||
|
// Soft-delete some of the docs, and one workspace.
|
||
|
await api.softDeleteDoc(doc11);
|
||
|
await api.softDeleteDoc(doc12);
|
||
|
await api.softDeleteDoc(doc13);
|
||
|
await api.softDeleteWorkspace(ws2);
|
||
|
|
||
|
// Check that nothing is deleted by housekeeper.
|
||
|
await keeper.deleteTrash();
|
||
|
await assert.isFulfilled(getDoc(doc11));
|
||
|
await assert.isFulfilled(getDoc(doc12));
|
||
|
await assert.isFulfilled(getDoc(doc13));
|
||
|
await assert.isFulfilled(getDoc(doc14));
|
||
|
await assert.isFulfilled(getDoc(doc21));
|
||
|
await assert.isFulfilled(getDoc(doc22));
|
||
|
await assert.isFulfilled(getWorkspace(ws1));
|
||
|
await assert.isFulfilled(getWorkspace(ws2));
|
||
|
|
||
|
// Age a doc and workspace somewhat, but not enough to trigger hard-deletion.
|
||
|
await ageDoc(doc11, 10);
|
||
|
await ageWorkspace(ws2, 20);
|
||
|
await keeper.deleteTrash();
|
||
|
await assert.isFulfilled(getDoc(doc11));
|
||
|
await assert.isFulfilled(getWorkspace(ws2));
|
||
|
|
||
|
// Prematurely age two of the soft-deleted docs, and the soft-deleted workspace.
|
||
|
await ageDoc(doc11, 40);
|
||
|
await ageDoc(doc12, 40);
|
||
|
await ageWorkspace(ws2, 40);
|
||
|
|
||
|
// Make sure that exactly those docs are deleted by housekeeper.
|
||
|
await keeper.deleteTrash();
|
||
|
await assert.isRejected(getDoc(doc11));
|
||
|
await assert.isRejected(getDoc(doc12));
|
||
|
await assert.isFulfilled(getDoc(doc13));
|
||
|
await assert.isFulfilled(getDoc(doc14));
|
||
|
await assert.isRejected(getDoc(doc21));
|
||
|
await assert.isRejected(getDoc(doc22));
|
||
|
await assert.isFulfilled(getWorkspace(ws1));
|
||
|
await assert.isRejected(getWorkspace(ws2));
|
||
|
});
|
||
|
|
||
|
it('enforces exclusivity of housekeeping', async function() {
|
||
|
const first = keeper.deleteTrashExclusively();
|
||
|
const second = keeper.deleteTrashExclusively();
|
||
|
assert.equal(await first, true);
|
||
|
assert.equal(await second, false);
|
||
|
assert.equal(await keeper.deleteTrashExclusively(), false);
|
||
|
await keeper.testClearExclusivity();
|
||
|
assert.equal(await keeper.deleteTrashExclusively(), true);
|
||
|
});
|
||
|
|
||
|
it('can delete old forks', async function() {
|
||
|
// Make a document with some forks.
|
||
|
const api = await home.createHomeApi('chimpy', org);
|
||
|
const ws3 = await api.newWorkspace({name: 'ws3'}, 'current');
|
||
|
const trunk = await api.newDoc({name: 'trunk'}, ws3);
|
||
|
const session = await api.getSessionActive();
|
||
|
const client = await openClient(home.server, session.user.email, session.org?.domain || 'docs');
|
||
|
await client.openDocOnConnect(trunk);
|
||
|
const forkResponse1 = await client.send('fork', 0);
|
||
|
const forkResponse2 = await client.send('fork', 0);
|
||
|
const forkPath1 = home.server.getStorageManager().getPath(forkResponse1.data.docId);
|
||
|
const forkPath2 = home.server.getStorageManager().getPath(forkResponse2.data.docId);
|
||
|
const forkId1 = forkResponse1.data.forkId;
|
||
|
const forkId2 = forkResponse2.data.forkId;
|
||
|
|
||
|
// Age the forks somewhat, but not enough to trigger hard-deletion.
|
||
|
await ageFork(forkId1, 10);
|
||
|
await ageFork(forkId2, 20);
|
||
|
await keeper.deleteTrash();
|
||
|
await assert.isFulfilled(getDoc(forkId1));
|
||
|
await assert.isFulfilled(getDoc(forkId2));
|
||
|
assert.equal(await fse.pathExists(forkPath1), true);
|
||
|
assert.equal(await fse.pathExists(forkPath2), true);
|
||
|
|
||
|
// Age one of the forks beyond the cleanup threshold.
|
||
|
await ageFork(forkId2, 40);
|
||
|
|
||
|
// Make sure that only that fork is deleted by housekeeper.
|
||
|
await keeper.deleteTrash();
|
||
|
await assert.isFulfilled(getDoc(forkId1));
|
||
|
await assert.isRejected(getDoc(forkId2));
|
||
|
assert.equal(await fse.pathExists(forkPath1), true);
|
||
|
assert.equal(await fse.pathExists(forkPath2), false);
|
||
|
});
|
||
|
|
||
|
it('can log metrics about sites', async function() {
|
||
|
const logMessages: [TelemetryEvent, TelemetryMetadataByLevel?][] = [];
|
||
|
sandbox.stub(Telemetry.prototype, 'shouldLogEvent').callsFake((name) => true);
|
||
|
sandbox.stub(Telemetry.prototype, 'logEvent').callsFake((_, name, meta) => {
|
||
|
// Skip document usage events that could be arriving in the
|
||
|
// middle of this test.
|
||
|
if (name !== 'documentUsage') {
|
||
|
logMessages.push([name, meta]);
|
||
|
}
|
||
|
return Promise.resolve();
|
||
|
});
|
||
|
await keeper.logMetrics();
|
||
|
assert.isNotEmpty(logMessages);
|
||
|
let [event, meta] = logMessages[0];
|
||
|
assert.equal(event, 'siteUsage');
|
||
|
assert.hasAllKeys(meta?.limited, [
|
||
|
'siteId',
|
||
|
'siteType',
|
||
|
'inGoodStanding',
|
||
|
'numDocs',
|
||
|
'numWorkspaces',
|
||
|
'numMembers',
|
||
|
'lastActivity',
|
||
|
'earliestDocCreatedAt',
|
||
|
]);
|
||
|
assert.hasAllKeys(meta?.full, [
|
||
|
'stripePlanId',
|
||
|
]);
|
||
|
[event, meta] = logMessages[logMessages.length - 1];
|
||
|
assert.equal(event, 'siteMembership');
|
||
|
assert.hasAllKeys(meta?.limited, [
|
||
|
'siteId',
|
||
|
'siteType',
|
||
|
'numOwners',
|
||
|
'numEditors',
|
||
|
'numViewers',
|
||
|
]);
|
||
|
assert.isUndefined(meta?.full);
|
||
|
});
|
||
|
});
|