mirror of
https://github.com/thelounge/thelounge
synced 2024-11-29 15:30:22 +00:00
899762cddd
This sets up the testing infrastructure to test migrations we are doing. It's done on a in memory database directly, we are only interested in the statements themselves and it's easier than to try and inject a prepared db into the store. We do add some dummy data though to make sure we actually execute the things as we expect.
306 lines
8.1 KiB
TypeScript
306 lines
8.1 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
|
import fs from "fs";
|
|
import path from "path";
|
|
import {expect} from "chai";
|
|
import util from "../util";
|
|
import Msg, {MessageType} from "../../server/models/msg";
|
|
import Config from "../../server/config";
|
|
import MessageStorage, {
|
|
currentSchemaVersion,
|
|
migrations,
|
|
necessaryMigrations,
|
|
} from "../../server/plugins/messageStorage/sqlite";
|
|
import Client from "../../server/client";
|
|
import sqlite3 from "sqlite3";
|
|
|
|
const orig_schema = [
|
|
// Schema version #1
|
|
// DO NOT CHANGE THIS IN ANY WAY, it's needed to properly test migrations
|
|
"CREATE TABLE IF NOT EXISTS options (name TEXT, value TEXT, CONSTRAINT name_unique UNIQUE (name))",
|
|
"CREATE TABLE IF NOT EXISTS messages (network TEXT, channel TEXT, time INTEGER, type TEXT, msg TEXT)",
|
|
"CREATE INDEX IF NOT EXISTS network_channel ON messages (network, channel)",
|
|
"CREATE INDEX IF NOT EXISTS time ON messages (time)",
|
|
];
|
|
|
|
const v1_schema_version = 1520239200;
|
|
|
|
const v1_dummy_messages = [
|
|
{
|
|
network: "8f650427-79a2-4950-b8af-94088b61b37c",
|
|
channel: "##linux",
|
|
time: 1594845354280,
|
|
type: "message",
|
|
msg: '{"from":{"mode":"","nick":"rascul"},"text":"db on a flash drive doesn\'t sound very nice though","self":false,"highlight":false,"users":[]}',
|
|
},
|
|
{
|
|
network: "8f650427-79a2-4950-b8af-94088b61b37c",
|
|
channel: "##linux",
|
|
time: 1594845357234,
|
|
type: "message",
|
|
msg: '{"from":{"mode":"","nick":"GrandPa-G"},"text":"that\'s the point of changing to make sure.","self":false,"highlight":false,"users":[]}',
|
|
},
|
|
{
|
|
network: "8f650427-79a2-4950-b8af-94088b61b37c",
|
|
channel: "#pleroma-dev",
|
|
time: 1594845358464,
|
|
type: "message",
|
|
msg: '{"from":{"mode":"@","nick":"rinpatch"},"text":"it\'s complicated","self":false,"highlight":false,"users":[]}',
|
|
},
|
|
];
|
|
|
|
describe("SQLite migrations", function () {
|
|
let db: sqlite3.Database;
|
|
|
|
function serialize_run(stmt: string, ...params: any[]): Promise<void> {
|
|
return new Promise((resolve, reject) => {
|
|
db.serialize(() => {
|
|
db.run(stmt, params, (err) => {
|
|
if (err) {
|
|
reject(err);
|
|
return;
|
|
}
|
|
|
|
resolve();
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
before(async function () {
|
|
db = new sqlite3.Database(":memory:");
|
|
|
|
for (const stmt of orig_schema) {
|
|
await serialize_run(stmt);
|
|
}
|
|
|
|
for (const msg of v1_dummy_messages) {
|
|
await serialize_run(
|
|
"INSERT INTO messages(network, channel, time, type, msg) VALUES(?, ?, ?, ?, ?)",
|
|
msg.network,
|
|
msg.channel,
|
|
msg.time,
|
|
msg.type,
|
|
msg.msg
|
|
);
|
|
}
|
|
});
|
|
|
|
after(function (done) {
|
|
db.close(done);
|
|
});
|
|
|
|
it("has working migrations", async function () {
|
|
const to_execute = necessaryMigrations(v1_schema_version);
|
|
expect(to_execute.length).to.eq(migrations.length);
|
|
await serialize_run("BEGIN EXCLUSIVE TRANSACTION");
|
|
|
|
for (const stmt of to_execute.map((m) => m.stmts).flat()) {
|
|
await serialize_run(stmt);
|
|
}
|
|
|
|
await serialize_run("COMMIT TRANSACTION");
|
|
});
|
|
});
|
|
|
|
describe("SQLite Message Storage", function () {
|
|
// Increase timeout due to unpredictable I/O on CI services
|
|
this.timeout(util.isRunningOnCI() ? 25000 : 5000);
|
|
this.slow(300);
|
|
|
|
const expectedPath = path.join(Config.getHomePath(), "logs", "testUser.sqlite3");
|
|
let store: MessageStorage;
|
|
|
|
before(function (done) {
|
|
store = new MessageStorage("testUser");
|
|
|
|
// Delete database file from previous test run
|
|
if (fs.existsSync(expectedPath)) {
|
|
fs.unlink(expectedPath, done);
|
|
} else {
|
|
done();
|
|
}
|
|
});
|
|
|
|
after(function (done) {
|
|
// After tests run, remove the logs folder
|
|
// so we return to the clean state
|
|
fs.unlinkSync(expectedPath);
|
|
fs.rmdir(path.join(Config.getHomePath(), "logs"), done);
|
|
});
|
|
|
|
it("should create database file", async function () {
|
|
expect(store.isEnabled).to.be.false;
|
|
expect(fs.existsSync(expectedPath)).to.be.false;
|
|
|
|
await store.enable();
|
|
expect(store.isEnabled).to.be.true;
|
|
});
|
|
|
|
it("should resolve an empty array when disabled", async function () {
|
|
store.isEnabled = false;
|
|
const messages = await store.getMessages(null as any, null as any, null as any);
|
|
expect(messages).to.be.empty;
|
|
store.isEnabled = true;
|
|
});
|
|
|
|
it("should insert schema version to options table", function (done) {
|
|
store.database.get(
|
|
"SELECT value FROM options WHERE name = 'schema_version'",
|
|
(err, row) => {
|
|
expect(err).to.be.null;
|
|
// compared as string because it's returned as such from the database
|
|
expect(row.value).to.equal(currentSchemaVersion.toString());
|
|
done();
|
|
}
|
|
);
|
|
});
|
|
|
|
it("should store a message", async function () {
|
|
await store.index(
|
|
{
|
|
uuid: "this-is-a-network-guid",
|
|
} as any,
|
|
{
|
|
name: "#thisISaCHANNEL",
|
|
} as any,
|
|
new Msg({
|
|
time: 123456789,
|
|
text: "Hello from sqlite world!",
|
|
} as any)
|
|
);
|
|
});
|
|
|
|
it("should retrieve previously stored message", async function () {
|
|
let msgid = 0;
|
|
const messages = await store.getMessages(
|
|
{
|
|
uuid: "this-is-a-network-guid",
|
|
} as any,
|
|
{
|
|
name: "#thisisaCHANNEL",
|
|
} as any,
|
|
() => msgid++
|
|
);
|
|
expect(messages).to.have.lengthOf(1);
|
|
const msg = messages[0];
|
|
expect(msg.text).to.equal("Hello from sqlite world!");
|
|
expect(msg.type).to.equal(MessageType.MESSAGE);
|
|
expect(msg.time.getTime()).to.equal(123456789);
|
|
});
|
|
|
|
it("should retrieve latest LIMIT messages in order", async function () {
|
|
const originalMaxHistory = Config.values.maxHistory;
|
|
|
|
try {
|
|
Config.values.maxHistory = 2;
|
|
|
|
for (let i = 0; i < 200; ++i) {
|
|
await store.index(
|
|
{uuid: "retrieval-order-test-network"} as any,
|
|
{name: "#channel"} as any,
|
|
new Msg({
|
|
time: 123456789 + i,
|
|
text: `msg ${i}`,
|
|
} as any)
|
|
);
|
|
}
|
|
|
|
let msgId = 0;
|
|
const messages = await store.getMessages(
|
|
{uuid: "retrieval-order-test-network"} as any,
|
|
{name: "#channel"} as any,
|
|
() => msgId++
|
|
);
|
|
expect(messages).to.have.lengthOf(2);
|
|
expect(messages.map((i_1) => i_1.text)).to.deep.equal(["msg 198", "msg 199"]);
|
|
} finally {
|
|
Config.values.maxHistory = originalMaxHistory;
|
|
}
|
|
});
|
|
|
|
it("should search messages", async function () {
|
|
const originalMaxHistory = Config.values.maxHistory;
|
|
|
|
try {
|
|
Config.values.maxHistory = 2;
|
|
|
|
const search = await store.search({
|
|
searchTerm: "msg",
|
|
networkUuid: "retrieval-order-test-network",
|
|
channelName: "",
|
|
offset: 0,
|
|
});
|
|
expect(search.results).to.have.lengthOf(100);
|
|
const expectedMessages: string[] = [];
|
|
|
|
for (let i = 100; i < 200; ++i) {
|
|
expectedMessages.push(`msg ${i}`);
|
|
}
|
|
|
|
expect(search.results.map((i_1) => i_1.text)).to.deep.equal(expectedMessages);
|
|
} finally {
|
|
Config.values.maxHistory = originalMaxHistory;
|
|
}
|
|
});
|
|
|
|
it("should search messages with escaped wildcards", async function () {
|
|
async function assertResults(query: string, expected: string[]) {
|
|
const search = await store.search({
|
|
searchTerm: query,
|
|
networkUuid: "this-is-a-network-guid2",
|
|
channelName: "",
|
|
offset: 0,
|
|
});
|
|
expect(search.results.map((i) => i.text)).to.deep.equal(expected);
|
|
}
|
|
|
|
const originalMaxHistory = Config.values.maxHistory;
|
|
|
|
try {
|
|
Config.values.maxHistory = 3;
|
|
|
|
await store.index(
|
|
{uuid: "this-is-a-network-guid2"} as any,
|
|
{name: "#channel"} as any,
|
|
new Msg({
|
|
time: 123456790,
|
|
text: `foo % bar _ baz`,
|
|
} as any)
|
|
);
|
|
|
|
await store.index(
|
|
{uuid: "this-is-a-network-guid2"} as any,
|
|
{name: "#channel"} as any,
|
|
new Msg({
|
|
time: 123456791,
|
|
text: `foo bar x baz`,
|
|
} as any)
|
|
);
|
|
|
|
await store.index(
|
|
{uuid: "this-is-a-network-guid2"} as any,
|
|
{name: "#channel"} as any,
|
|
new Msg({
|
|
time: 123456792,
|
|
text: `bar @ baz`,
|
|
} as any)
|
|
);
|
|
|
|
await assertResults("foo", ["foo % bar _ baz", "foo bar x baz"]);
|
|
await assertResults("%", ["foo % bar _ baz"]);
|
|
await assertResults("foo % bar ", ["foo % bar _ baz"]);
|
|
await assertResults("_", ["foo % bar _ baz"]);
|
|
await assertResults("bar _ baz", ["foo % bar _ baz"]);
|
|
await assertResults("%%", []);
|
|
await assertResults("@%", []);
|
|
await assertResults("@", ["bar @ baz"]);
|
|
} finally {
|
|
Config.values.maxHistory = originalMaxHistory;
|
|
}
|
|
});
|
|
|
|
it("should close database", async function () {
|
|
await store.close();
|
|
expect(fs.existsSync(expectedPath)).to.be.true;
|
|
});
|
|
});
|