/* 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 { 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; }); });