// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. import Foundation import GRDB import Quick import Nimble import SessionUtilitiesKit @testable import SessionMessagingKit class SessionThreadViewModelSpec: QuickSpec { override class func spec() { // MARK: Configuration @TestState var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), initialData: { db in try db.create(table: TestMessage.self) { t in t.column(.body, .text).notNull() } try db.create(virtualTable: TestMessage.fullTextSearchTableName, using: FTS5()) { t in t.synchronize(withTable: TestMessage.databaseTableName) t.tokenizer = .porter(wrapping: .unicode61()) t.column(TestMessage.Columns.body.name) } } ) // MARK: - a SessionThreadViewModel describe("a SessionThreadViewModel") { // MARK: -- when processing a search term context("when processing a search term") { // MARK: ---- correctly generates a safe search term it("correctly generates a safe search term") { expect(SessionThreadViewModel.searchSafeTerm("Test")).to(equal("\"Test\"")) } // MARK: ---- standardises odd quote characters it("standardises odd quote characters") { expect(SessionThreadViewModel.standardQuotes("\"")).to(equal("\"")) expect(SessionThreadViewModel.standardQuotes("”")).to(equal("\"")) expect(SessionThreadViewModel.standardQuotes("“")).to(equal("\"")) } // MARK: ---- splits on the space character it("splits on the space character") { expect(SessionThreadViewModel.searchTermParts("Test Message")) .to(equal([ "\"Test\"", "\"Message\"" ])) } // MARK: ---- surrounds each split term with quotes it("surrounds each split term with quotes") { expect(SessionThreadViewModel.searchTermParts("Test Message")) .to(equal([ "\"Test\"", "\"Message\"" ])) } // MARK: ---- keeps words within quotes together it("keeps words within quotes together") { expect(SessionThreadViewModel.searchTermParts("This \"is a Test\" Message")) .to(equal([ "\"This\"", "\"is a Test\"", "\"Message\"" ])) expect(SessionThreadViewModel.searchTermParts("\"This is\" a Test Message")) .to(equal([ "\"This is\"", "\"a\"", "\"Test\"", "\"Message\"" ])) expect(SessionThreadViewModel.searchTermParts("\"This is\" \"a Test\" Message")) .to(equal([ "\"This is\"", "\"a Test\"", "\"Message\"" ])) expect(SessionThreadViewModel.searchTermParts("\"This is\" a \"Test Message\"")) .to(equal([ "\"This is\"", "\"a\"", "\"Test Message\"" ])) expect(SessionThreadViewModel.searchTermParts("\"This is\"\" a \"Test Message")) .to(equal([ "\"This is\"", "\" a \"", "\"Test\"", "\"Message\"" ])) } // MARK: ---- keeps words within weird quotes together it("keeps words within weird quotes together") { expect(SessionThreadViewModel.searchTermParts("This ”is a Test“ Message")) .to(equal([ "\"This\"", "\"is a Test\"", "\"Message\"" ])) } // MARK: ---- removes extra whitespace it("removes extra whitespace") { expect(SessionThreadViewModel.searchTermParts(" Test Message ")) .to(equal([ "\"Test\"", "\"Message\"" ])) } } // MARK: -- when searching context("when searching") { beforeEach { mockStorage.write { db in try TestMessage(body: "Test").insert(db) try TestMessage(body: "Test123").insert(db) try TestMessage(body: "Test234").insert(db) try TestMessage(body: "Test Test123").insert(db) try TestMessage(body: "Test Test123 Test234").insert(db) try TestMessage(body: "Test Test234").insert(db) try TestMessage(body: "Test Test234 Test123").insert(db) try TestMessage(body: "This is a Test Message").insert(db) try TestMessage(body: "is a Message This Test").insert(db) try TestMessage(body: "this message is a test").insert(db) try TestMessage( body: "This content is something which includes a combination of test words found in another message" ) .insert(db) try TestMessage(body: "Do test messages contain content?").insert(db) try TestMessage(body: "Is messaging awesome?").insert(db) } } // MARK: ---- returns results it("returns results") { let results = mockStorage.read { db in let pattern: FTS5Pattern = try SessionThreadViewModel.pattern( db, searchTerm: "Message", forTable: TestMessage.self ) return try SQLRequest(literal: """ SELECT * FROM testMessage JOIN testMessage_fts ON ( testMessage_fts.rowId = testMessage.rowId AND testMessage_fts.body MATCH \(pattern) ) """).fetchAll(db) } expect(results) .to(equal([ TestMessage(body: "This is a Test Message"), TestMessage(body: "is a Message This Test"), TestMessage(body: "this message is a test"), TestMessage(body: "This content is something which includes a combination of test words found in another message"), TestMessage(body: "Do test messages contain content?"), TestMessage(body: "Is messaging awesome?") ])) } // MARK: ---- adds a wildcard to the final part it("adds a wildcard to the final part") { let results = mockStorage.read { db in let pattern: FTS5Pattern = try SessionThreadViewModel.pattern( db, searchTerm: "This mes", forTable: TestMessage.self ) return try SQLRequest(literal: """ SELECT * FROM testMessage JOIN testMessage_fts ON ( testMessage_fts.rowId = testMessage.rowId AND testMessage_fts.body MATCH \(pattern) ) """).fetchAll(db) } expect(results) .to(equal([ TestMessage(body: "This is a Test Message"), TestMessage(body: "is a Message This Test"), TestMessage(body: "this message is a test"), TestMessage(body: "This content is something which includes a combination of test words found in another message"), TestMessage(body: "Do test messages contain content?"), TestMessage(body: "Is messaging awesome?") ])) } // MARK: ---- does not add a wildcard to other parts it("does not add a wildcard to other parts") { let results = mockStorage.read { db in let pattern: FTS5Pattern = try SessionThreadViewModel.pattern( db, searchTerm: "mes Random", forTable: TestMessage.self ) return try SQLRequest(literal: """ SELECT * FROM testMessage JOIN testMessage_fts ON ( testMessage_fts.rowId = testMessage.rowId AND testMessage_fts.body MATCH \(pattern) ) """).fetchAll(db) } expect(results) .to(beEmpty()) } // MARK: ---- finds similar words without the wildcard due to the porter tokenizer it("finds similar words without the wildcard due to the porter tokenizer") { let results = mockStorage.read { db in let pattern: FTS5Pattern = try SessionThreadViewModel.pattern( db, searchTerm: "message z", forTable: TestMessage.self ) return try SQLRequest(literal: """ SELECT * FROM testMessage JOIN testMessage_fts ON ( testMessage_fts.rowId = testMessage.rowId AND testMessage_fts.body MATCH \(pattern) ) """).fetchAll(db) } expect(results) .to(equal([ TestMessage(body: "This is a Test Message"), TestMessage(body: "is a Message This Test"), TestMessage(body: "this message is a test"), TestMessage( body: "This content is something which includes a combination of test words found in another message" ), TestMessage(body: "Do test messages contain content?"), TestMessage(body: "Is messaging awesome?") ])) } // MARK: ---- finds results containing the words regardless of the order it("finds results containing the words regardless of the order") { let results = mockStorage.read { db in let pattern: FTS5Pattern = try SessionThreadViewModel.pattern( db, searchTerm: "is a message", forTable: TestMessage.self ) return try SQLRequest(literal: """ SELECT * FROM testMessage JOIN testMessage_fts ON ( testMessage_fts.rowId = testMessage.rowId AND testMessage_fts.body MATCH \(pattern) ) """).fetchAll(db) } expect(results) .to(equal([ TestMessage(body: "This is a Test Message"), TestMessage(body: "is a Message This Test"), TestMessage(body: "this message is a test"), TestMessage( body: "This content is something which includes a combination of test words found in another message" ), TestMessage(body: "Do test messages contain content?"), TestMessage(body: "Is messaging awesome?") ])) } // MARK: ---- does not find quoted parts out of order it("does not find quoted parts out of order") { let results = mockStorage.read { db in let pattern: FTS5Pattern = try SessionThreadViewModel.pattern( db, searchTerm: "\"this is a\" \"test message\"", forTable: TestMessage.self ) return try SQLRequest(literal: """ SELECT * FROM testMessage JOIN testMessage_fts ON ( testMessage_fts.rowId = testMessage.rowId AND testMessage_fts.body MATCH \(pattern) ) """).fetchAll(db) } expect(results) .to(equal([ TestMessage(body: "This is a Test Message"), TestMessage(body: "Do test messages contain content?") ])) } } } } } // MARK: - Test Types fileprivate struct TestMessage: Codable, Equatable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible { public static var databaseTableName: String { "testMessage" } public typealias Columns = CodingKeys public enum CodingKeys: String, CodingKey, ColumnExpression { case body } public let body: String }