2024-01-11 09:40:55 +03:00
|
|
|
import { TestingModule } from '@nestjs/testing';
|
2023-11-22 10:56:59 +03:00
|
|
|
import type { Snapshot } from '@prisma/client';
|
2024-01-12 07:18:39 +03:00
|
|
|
import { PrismaClient } from '@prisma/client';
|
2023-11-22 10:56:59 +03:00
|
|
|
import test from 'ava';
|
|
|
|
import * as Sinon from 'sinon';
|
|
|
|
|
2024-01-22 10:40:28 +03:00
|
|
|
import { DocHistoryManager } from '../src/core/doc';
|
|
|
|
import { QuotaModule } from '../src/core/quota';
|
|
|
|
import { StorageModule } from '../src/core/storage';
|
2024-01-12 07:18:39 +03:00
|
|
|
import { type EventPayload } from '../src/fundamentals/event';
|
2024-01-11 09:40:55 +03:00
|
|
|
import { createTestingModule } from './utils';
|
2023-11-22 10:56:59 +03:00
|
|
|
|
|
|
|
let m: TestingModule;
|
|
|
|
let manager: DocHistoryManager;
|
2024-01-12 07:18:39 +03:00
|
|
|
let db: PrismaClient;
|
2023-11-22 10:56:59 +03:00
|
|
|
|
|
|
|
// cleanup database before each test
|
|
|
|
test.beforeEach(async () => {
|
2024-01-11 09:40:55 +03:00
|
|
|
m = await createTestingModule({
|
|
|
|
imports: [StorageModule, QuotaModule],
|
2023-11-22 10:56:59 +03:00
|
|
|
providers: [DocHistoryManager],
|
2024-01-11 09:40:55 +03:00
|
|
|
});
|
2023-11-22 10:56:59 +03:00
|
|
|
|
|
|
|
manager = m.get(DocHistoryManager);
|
|
|
|
Sinon.stub(manager, 'getExpiredDateFromNow').resolves(
|
|
|
|
new Date(Date.now() + 1000)
|
|
|
|
);
|
2024-01-12 07:18:39 +03:00
|
|
|
db = m.get(PrismaClient);
|
2023-11-22 10:56:59 +03:00
|
|
|
});
|
|
|
|
|
2024-01-11 09:40:55 +03:00
|
|
|
test.afterEach.always(async () => {
|
2023-11-22 10:56:59 +03:00
|
|
|
await m.close();
|
|
|
|
Sinon.restore();
|
|
|
|
});
|
|
|
|
|
|
|
|
const snapshot: Snapshot = {
|
|
|
|
workspaceId: '1',
|
|
|
|
id: 'doc1',
|
2023-12-08 08:00:58 +03:00
|
|
|
blob: Buffer.from([1, 0]),
|
|
|
|
state: Buffer.from([0]),
|
2023-11-22 10:56:59 +03:00
|
|
|
seq: 0,
|
|
|
|
updatedAt: new Date(),
|
|
|
|
createdAt: new Date(),
|
|
|
|
};
|
|
|
|
|
2023-12-08 08:00:58 +03:00
|
|
|
function getEventData(
|
|
|
|
timestamp: Date = new Date()
|
|
|
|
): EventPayload<'snapshot.updated'> {
|
|
|
|
return {
|
|
|
|
workspaceId: snapshot.workspaceId,
|
|
|
|
id: snapshot.id,
|
|
|
|
previous: { ...snapshot, updatedAt: timestamp },
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-11-22 10:56:59 +03:00
|
|
|
test('should create doc history if never created before', async t => {
|
|
|
|
Sinon.stub(manager, 'last').resolves(null);
|
|
|
|
|
|
|
|
const timestamp = new Date();
|
2023-12-08 08:00:58 +03:00
|
|
|
await manager.onDocUpdated(getEventData(timestamp));
|
2023-11-22 10:56:59 +03:00
|
|
|
|
|
|
|
const history = await db.snapshotHistory.findFirst({
|
|
|
|
where: {
|
|
|
|
workspaceId: '1',
|
|
|
|
id: 'doc1',
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
t.truthy(history);
|
|
|
|
t.is(history?.timestamp.getTime(), timestamp.getTime());
|
|
|
|
});
|
|
|
|
|
2023-11-23 10:39:02 +03:00
|
|
|
test('should not create history if timestamp equals to last record', async t => {
|
2023-11-22 10:56:59 +03:00
|
|
|
const timestamp = new Date();
|
2023-11-23 10:39:02 +03:00
|
|
|
Sinon.stub(manager, 'last').resolves({ timestamp, state: null });
|
2023-11-22 10:56:59 +03:00
|
|
|
|
2023-12-08 08:00:58 +03:00
|
|
|
await manager.onDocUpdated(getEventData(timestamp));
|
2023-11-22 10:56:59 +03:00
|
|
|
|
|
|
|
const history = await db.snapshotHistory.findFirst({
|
|
|
|
where: {
|
|
|
|
workspaceId: '1',
|
|
|
|
id: 'doc1',
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
t.falsy(history);
|
|
|
|
});
|
|
|
|
|
2023-11-23 10:39:02 +03:00
|
|
|
test('should not create history if state equals to last record', async t => {
|
2023-11-22 10:56:59 +03:00
|
|
|
const timestamp = new Date();
|
|
|
|
Sinon.stub(manager, 'last').resolves({
|
2023-11-23 10:39:02 +03:00
|
|
|
timestamp: new Date(timestamp.getTime() - 1),
|
|
|
|
state: snapshot.state,
|
2023-11-22 10:56:59 +03:00
|
|
|
});
|
|
|
|
|
2023-12-08 08:00:58 +03:00
|
|
|
await manager.onDocUpdated(getEventData(timestamp));
|
2023-11-22 10:56:59 +03:00
|
|
|
|
|
|
|
const history = await db.snapshotHistory.findFirst({
|
|
|
|
where: {
|
|
|
|
workspaceId: '1',
|
|
|
|
id: 'doc1',
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2023-11-23 10:39:02 +03:00
|
|
|
t.falsy(history);
|
2023-11-22 10:56:59 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
test('should not create history if time diff is less than interval config', async t => {
|
|
|
|
const timestamp = new Date();
|
|
|
|
Sinon.stub(manager, 'last').resolves({
|
|
|
|
timestamp: new Date(timestamp.getTime() - 1000),
|
2023-11-23 10:39:02 +03:00
|
|
|
state: Buffer.from([0, 1]),
|
2023-11-22 10:56:59 +03:00
|
|
|
});
|
|
|
|
|
2023-12-08 08:00:58 +03:00
|
|
|
await manager.onDocUpdated(getEventData(timestamp));
|
2023-11-22 10:56:59 +03:00
|
|
|
|
|
|
|
const history = await db.snapshotHistory.findFirst({
|
|
|
|
where: {
|
|
|
|
workspaceId: '1',
|
|
|
|
id: 'doc1',
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
t.falsy(history);
|
|
|
|
});
|
|
|
|
|
2023-11-23 10:39:02 +03:00
|
|
|
test('should create history if time diff is larger than interval config and state diff', async t => {
|
|
|
|
const timestamp = new Date();
|
|
|
|
Sinon.stub(manager, 'last').resolves({
|
|
|
|
timestamp: new Date(timestamp.getTime() - 1000 * 60 * 20),
|
|
|
|
state: Buffer.from([0, 1]),
|
|
|
|
});
|
|
|
|
|
2023-12-08 08:00:58 +03:00
|
|
|
await manager.onDocUpdated(getEventData(timestamp));
|
2023-11-23 10:39:02 +03:00
|
|
|
|
|
|
|
const history = await db.snapshotHistory.findFirst({
|
|
|
|
where: {
|
|
|
|
workspaceId: '1',
|
|
|
|
id: 'doc1',
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
t.truthy(history);
|
|
|
|
});
|
|
|
|
|
2023-11-22 10:56:59 +03:00
|
|
|
test('should create history with force flag even if time diff in small', async t => {
|
|
|
|
const timestamp = new Date();
|
|
|
|
Sinon.stub(manager, 'last').resolves({
|
|
|
|
timestamp: new Date(timestamp.getTime() - 1),
|
2023-11-23 10:39:02 +03:00
|
|
|
state: Buffer.from([0, 1]),
|
2023-11-22 10:56:59 +03:00
|
|
|
});
|
|
|
|
|
2023-12-08 08:00:58 +03:00
|
|
|
await manager.onDocUpdated(getEventData(timestamp), true);
|
2023-11-22 10:56:59 +03:00
|
|
|
|
|
|
|
const history = await db.snapshotHistory.findFirst({
|
|
|
|
where: {
|
|
|
|
workspaceId: '1',
|
|
|
|
id: 'doc1',
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
t.truthy(history);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('should correctly list all history records', async t => {
|
|
|
|
const timestamp = Date.now();
|
|
|
|
|
|
|
|
// insert expired data
|
|
|
|
await db.snapshotHistory.createMany({
|
2023-11-29 07:44:06 +03:00
|
|
|
data: Array.from({ length: 10 })
|
|
|
|
.fill(0)
|
|
|
|
.map((_, i) => ({
|
|
|
|
workspaceId: snapshot.workspaceId,
|
|
|
|
id: snapshot.id,
|
|
|
|
blob: snapshot.blob,
|
|
|
|
state: snapshot.state,
|
|
|
|
timestamp: new Date(timestamp - 10 - i),
|
|
|
|
expiredAt: new Date(timestamp - 1),
|
|
|
|
})),
|
2023-11-22 10:56:59 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
// insert available data
|
|
|
|
await db.snapshotHistory.createMany({
|
2023-11-29 07:44:06 +03:00
|
|
|
data: Array.from({ length: 10 })
|
|
|
|
.fill(0)
|
|
|
|
.map((_, i) => ({
|
|
|
|
workspaceId: snapshot.workspaceId,
|
|
|
|
id: snapshot.id,
|
|
|
|
blob: snapshot.blob,
|
|
|
|
state: snapshot.state,
|
|
|
|
timestamp: new Date(timestamp + i),
|
|
|
|
expiredAt: new Date(timestamp + 1000),
|
|
|
|
})),
|
2023-11-22 10:56:59 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
const list = await manager.list(
|
|
|
|
snapshot.workspaceId,
|
|
|
|
snapshot.id,
|
|
|
|
new Date(timestamp + 20),
|
|
|
|
8
|
|
|
|
);
|
|
|
|
const count = await manager.count(snapshot.workspaceId, snapshot.id);
|
|
|
|
|
|
|
|
t.is(list.length, 8);
|
|
|
|
t.is(count, 10);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('should be able to get history data', async t => {
|
|
|
|
const timestamp = new Date();
|
|
|
|
|
2023-12-08 08:00:58 +03:00
|
|
|
await manager.onDocUpdated(getEventData(timestamp), true);
|
2023-11-22 10:56:59 +03:00
|
|
|
|
|
|
|
const history = await manager.get(
|
|
|
|
snapshot.workspaceId,
|
|
|
|
snapshot.id,
|
|
|
|
timestamp
|
|
|
|
);
|
|
|
|
|
|
|
|
t.truthy(history);
|
|
|
|
t.deepEqual(history?.blob, snapshot.blob);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('should be able to get last history record', async t => {
|
|
|
|
const timestamp = Date.now();
|
|
|
|
|
|
|
|
// insert available data
|
|
|
|
await db.snapshotHistory.createMany({
|
2023-11-29 07:44:06 +03:00
|
|
|
data: Array.from({ length: 10 })
|
|
|
|
.fill(0)
|
|
|
|
.map((_, i) => ({
|
|
|
|
workspaceId: snapshot.workspaceId,
|
|
|
|
id: snapshot.id,
|
|
|
|
blob: snapshot.blob,
|
|
|
|
state: snapshot.state,
|
|
|
|
timestamp: new Date(timestamp + i),
|
|
|
|
expiredAt: new Date(timestamp + 1000),
|
|
|
|
})),
|
2023-11-22 10:56:59 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
const history = await manager.last(snapshot.workspaceId, snapshot.id);
|
|
|
|
|
|
|
|
t.truthy(history);
|
|
|
|
t.is(history?.timestamp.getTime(), timestamp + 9);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('should be able to recover from history', async t => {
|
|
|
|
await db.snapshot.create({
|
|
|
|
data: {
|
|
|
|
...snapshot,
|
|
|
|
blob: Buffer.from([1, 1]),
|
|
|
|
state: Buffer.from([1, 1]),
|
|
|
|
},
|
|
|
|
});
|
|
|
|
const history1Timestamp = snapshot.updatedAt.getTime() - 10;
|
2023-12-08 08:00:58 +03:00
|
|
|
await manager.onDocUpdated(getEventData(new Date(history1Timestamp)));
|
2023-11-22 10:56:59 +03:00
|
|
|
|
|
|
|
await manager.recover(
|
|
|
|
snapshot.workspaceId,
|
|
|
|
snapshot.id,
|
|
|
|
new Date(history1Timestamp)
|
|
|
|
);
|
|
|
|
|
|
|
|
const [history1, history2] = await db.snapshotHistory.findMany({
|
|
|
|
where: {
|
|
|
|
workspaceId: snapshot.workspaceId,
|
|
|
|
id: snapshot.id,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
t.is(history1.timestamp.getTime(), history1Timestamp);
|
|
|
|
t.is(history2.timestamp.getTime(), snapshot.updatedAt.getTime());
|
|
|
|
|
|
|
|
// new history data force created with snapshot state before recovered
|
2023-12-14 12:50:46 +03:00
|
|
|
t.deepEqual(history2.blob, Buffer.from([1, 1]));
|
|
|
|
t.deepEqual(history2.state, Buffer.from([1, 1]));
|
2023-11-22 10:56:59 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
test('should be able to cleanup expired history', async t => {
|
|
|
|
const timestamp = Date.now();
|
|
|
|
|
|
|
|
// insert expired data
|
|
|
|
await db.snapshotHistory.createMany({
|
2023-11-29 07:44:06 +03:00
|
|
|
data: Array.from({ length: 10 })
|
|
|
|
.fill(0)
|
|
|
|
.map((_, i) => ({
|
|
|
|
workspaceId: snapshot.workspaceId,
|
|
|
|
id: snapshot.id,
|
|
|
|
blob: snapshot.blob,
|
|
|
|
state: snapshot.state,
|
|
|
|
timestamp: new Date(timestamp - 10 - i),
|
|
|
|
expiredAt: new Date(timestamp - 1),
|
|
|
|
})),
|
2023-11-22 10:56:59 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
// insert available data
|
|
|
|
await db.snapshotHistory.createMany({
|
2023-11-29 07:44:06 +03:00
|
|
|
data: Array.from({ length: 10 })
|
|
|
|
.fill(0)
|
|
|
|
.map((_, i) => ({
|
|
|
|
workspaceId: snapshot.workspaceId,
|
|
|
|
id: snapshot.id,
|
|
|
|
blob: snapshot.blob,
|
|
|
|
state: snapshot.state,
|
|
|
|
timestamp: new Date(timestamp + i),
|
|
|
|
expiredAt: new Date(timestamp + 1000),
|
|
|
|
})),
|
2023-11-22 10:56:59 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
let count = await db.snapshotHistory.count();
|
|
|
|
t.is(count, 20);
|
|
|
|
|
|
|
|
await manager.cleanupExpiredHistory();
|
|
|
|
|
|
|
|
count = await db.snapshotHistory.count();
|
|
|
|
t.is(count, 10);
|
2023-11-23 10:39:02 +03:00
|
|
|
|
|
|
|
const example = await db.snapshotHistory.findFirst();
|
|
|
|
t.truthy(example);
|
|
|
|
t.true(example!.expiredAt > new Date());
|
2023-11-22 10:56:59 +03:00
|
|
|
});
|