From b77ad94415f9820d2bddefa2e93f81d4ed1816f6 Mon Sep 17 00:00:00 2001 From: "Sven A. Schmidt" Date: Tue, 4 Mar 2025 11:03:13 +0100 Subject: [PATCH 1/5] Convert SearchFilterTests --- Tests/AppTests/AppTestCase.swift | 30 ++ Tests/AppTests/SearchFilterTests.swift | 614 ++++++++++++------------- 2 files changed, 334 insertions(+), 310 deletions(-) diff --git a/Tests/AppTests/AppTestCase.swift b/Tests/AppTests/AppTestCase.swift index c45b81222..9afeb7d66 100644 --- a/Tests/AppTests/AppTestCase.swift +++ b/Tests/AppTests/AppTestCase.swift @@ -15,6 +15,7 @@ @testable import App import Dependencies +import Fluent import NIOConcurrencyHelpers import PostgresNIO import SQLKit @@ -181,6 +182,35 @@ extension AppTestCase { } +// FIXME: Move this once AppTestCase can be removed. These are helpers created during the transition to Swift Testing. +extension Database { + func renderSQL(_ query: SQLExpression) -> String { + var serializer = SQLSerializer(database: self as! SQLDatabase) + query.serialize(to: &serializer) + return serializer.sql + } + + func binds(_ query: SQLExpression?) -> [String] { + var serializer = SQLSerializer(database: self as! SQLDatabase) + query?.serialize(to: &serializer) + return serializer.binds.reduce(into: []) { result, bind in + switch bind { + case let bind as Date: + result.append(DateFormatter.filterParseFormatter.string(from: bind)) + case let bind as Set: + let s = bind.map(\.rawValue).sorted().joined(separator: ",") + result.append("{\(s)}") + case let bind as Set: + let s = bind.map(\.rawValue).sorted().joined(separator: ",") + result.append("{\(s)}") + default: + result.append("\(bind)") + } + } + } +} + + private func connect(to databaseName: String, _ environment: Environment) async throws -> PostgresClient { await DotEnvFile.load(for: environment, fileio: .init(threadPool: .singleton)) let host = Environment.get("DATABASE_HOST")! diff --git a/Tests/AppTests/SearchFilterTests.swift b/Tests/AppTests/SearchFilterTests.swift index 7bd29f2af..6ee1b264a 100644 --- a/Tests/AppTests/SearchFilterTests.swift +++ b/Tests/AppTests/SearchFilterTests.swift @@ -16,17 +16,16 @@ import SnapshotTesting import SQLKit -import XCTest -import XCTVapor +import Testing -class SearchFilterTests: AppTestCase { +@Suite struct SearchFilterTests { - func test_SearchFilterKey_searchFilter() throws { + @Test func SearchFilterKey_searchFilter() throws { // Ensure all `SearchFilter.Key`s are wired correctly to their // `SearchFilterProtocol.Type`s (by roundtripping through the key values) - XCTAssertEqual(SearchFilter.Key.allCases - .map { $0.searchFilter.key }, [ + #expect(SearchFilter.Key.allCases + .map { $0.searchFilter.key } == [ .author, .keyword, .lastActivity, @@ -38,417 +37,413 @@ class SearchFilterTests: AppTestCase { ]) } - func test_Expression_init() throws { - XCTAssertEqual(SearchFilter.Expression(predicate: ">5"), - .init(operator: .greaterThan, value: "5")) - XCTAssertEqual(SearchFilter.Expression(predicate: ">=5"), - .init(operator: .greaterThanOrEqual, value: "5")) - XCTAssertEqual(SearchFilter.Expression(predicate: "<5"), - .init(operator: .lessThan, value: "5")) - XCTAssertEqual(SearchFilter.Expression(predicate: "<=5"), - .init(operator: .lessThanOrEqual, value: "5")) - XCTAssertEqual(SearchFilter.Expression(predicate: "!5"), - .init(operator: .isNot, value: "5")) - XCTAssertEqual(SearchFilter.Expression(predicate: "5"), - .init(operator: .is, value: "5")) - XCTAssertEqual(SearchFilter.Expression(predicate: ""), nil) - XCTAssertEqual(SearchFilter.Expression(predicate: "!with space"), - .init(operator: .isNot, value: "with space")) - } - - func test_parse() { + @Test func Expression_init() throws { + #expect(SearchFilter.Expression(predicate: ">5") == .init(operator: .greaterThan, value: "5")) + #expect(SearchFilter.Expression(predicate: ">=5") == .init(operator: .greaterThanOrEqual, value: "5")) + #expect(SearchFilter.Expression(predicate: "<5") == .init(operator: .lessThan, value: "5")) + #expect(SearchFilter.Expression(predicate: "<=5") == .init(operator: .lessThanOrEqual, value: "5")) + #expect(SearchFilter.Expression(predicate: "!5") == .init(operator: .isNot, value: "5")) + #expect(SearchFilter.Expression(predicate: "5") == .init(operator: .is, value: "5")) + #expect(SearchFilter.Expression(predicate: "") == nil) + #expect(SearchFilter.Expression(predicate: "!with space") == .init(operator: .isNot, value: "with space")) + } + + @Test func parse() { do { // No colon - XCTAssertNil(SearchFilter.parse(filterTerm: "a")) + #expect(SearchFilter.parse(filterTerm: "a") == nil) } do { // Too many colons - XCTAssertNil(SearchFilter.parse(filterTerm: "a:b:c")) + #expect(SearchFilter.parse(filterTerm: "a:b:c") == nil) } do { // No valid filter - XCTAssertNil(SearchFilter.parse(filterTerm: "invalid:true")) + #expect(SearchFilter.parse(filterTerm: "invalid:true") == nil) } do { // Valid filter - XCTAssertTrue(SearchFilter.parse(filterTerm: "stars:5") is StarsSearchFilter) + #expect(SearchFilter.parse(filterTerm: "stars:5") is StarsSearchFilter) } } - func test_separateTermsAndFilters() { + @Test func separateTermsAndFilters() { let output = SearchFilter.split(terms: ["a", "b", "invalid:true", "stars:5"]) - XCTAssertEqual(output.terms.sorted(), ["a", "b", "invalid:true"]) + #expect(output.terms.sorted() == ["a", "b", "invalid:true"]) - XCTAssertEqual(output.filters.count, 1) - XCTAssertTrue(output.filters[0] is StarsSearchFilter) + #expect(output.filters.count == 1) + #expect(output.filters[0] is StarsSearchFilter) } // MARK: Filters - func test_authorFilter() throws { - let filter = try AuthorSearchFilter(expression: .init(operator: .is, - value: "sherlouk")) - XCTAssertEqual(filter.key, .author) - XCTAssertEqual(filter.predicate, .init(operator: .caseInsensitiveLike, - bindableValue: .value("sherlouk"), - displayValue: "sherlouk")) - - // test view representation - XCTAssertEqual(filter.viewModel.description, "author is sherlouk") - - // test sql representation - XCTAssertEqual(renderSQL(filter.leftHandSide), #""repo_owner""#) - XCTAssertEqual(renderSQL(filter.sqlOperator), "ILIKE") - XCTAssertEqual(binds(filter.rightHandSide), ["sherlouk"]) - - // test error case - XCTAssertThrowsError(try AuthorSearchFilter(expression: .init(operator: .greaterThan, - value: "sherlouk"))) { - XCTAssertEqual($0 as? SearchFilterError, .unsupportedComparisonMethod) + @Test func authorFilter() async throws { + try await withApp { app in + let filter = try AuthorSearchFilter(expression: .init(operator: .is, + value: "sherlouk")) + #expect(filter.key == .author) + #expect(filter.predicate == .init(operator: .caseInsensitiveLike, + bindableValue: .value("sherlouk"), + displayValue: "sherlouk")) + + // test view representation + #expect(filter.viewModel.description == "author is sherlouk") + + // test sql representation + #expect(app.db.renderSQL(filter.leftHandSide) == #""repo_owner""#) + #expect(app.db.renderSQL(filter.sqlOperator) == "ILIKE") + #expect(app.db.binds(filter.rightHandSide) == ["sherlouk"]) + #expect { + try AuthorSearchFilter(expression: .init(operator: .greaterThan, value: "sherlouk")) + } throws: { + $0 as? SearchFilterError == .unsupportedComparisonMethod + } } } - func test_keywordFilter() throws { - let filter = try KeywordSearchFilter(expression: .init(operator: .is, - value: "cache")) - XCTAssertEqual(filter.key, .keyword) - XCTAssertEqual(filter.predicate, .init(operator: .caseInsensitiveLike, - bindableValue: .value("cache"), - displayValue: "cache")) - - // test view representation - XCTAssertEqual(filter.viewModel.description, "keywords is cache") - - // test sql representation - XCTAssertEqual(renderSQL(filter.leftHandSide), "$1") - XCTAssertEqual(binds(filter.leftHandSide), ["cache"]) - XCTAssertEqual(renderSQL(filter.sqlOperator), "ILIKE") - XCTAssertEqual(renderSQL(filter.rightHandSide), #"ANY("keywords")"#) - - // test error case - XCTAssertThrowsError(try KeywordSearchFilter(expression: .init(operator: .greaterThan, - value: "cache"))) { - XCTAssertEqual($0 as? SearchFilterError, .unsupportedComparisonMethod) + @Test func keywordFilter() async throws { + try await withApp { app in + let filter = try KeywordSearchFilter(expression: .init(operator: .is, + value: "cache")) + #expect(filter.key == .keyword) + #expect(filter.predicate == .init(operator: .caseInsensitiveLike, + bindableValue: .value("cache"), + displayValue: "cache")) + + // test view representation + #expect(filter.viewModel.description == "keywords is cache") + + // test sql representation + #expect(app.db.renderSQL(filter.leftHandSide) == "$1") + #expect(app.db.binds(filter.leftHandSide) == ["cache"]) + #expect(app.db.renderSQL(filter.sqlOperator) == "ILIKE") + #expect(app.db.renderSQL(filter.rightHandSide) == #"ANY("keywords")"#) + #expect { + try KeywordSearchFilter(expression: .init(operator: .greaterThan, value: "cache")) + } throws: { + $0 as? SearchFilterError == .unsupportedComparisonMethod + } } } - func test_lastActivityFilter() throws { - let filter = try LastActivitySearchFilter(expression: .init(operator: .is, - value: "1970-01-01")) - XCTAssertEqual(filter.key, .lastActivity) - XCTAssertEqual(filter.predicate, .init(operator: .equal, - bindableValue: .value("1970-01-01"), - displayValue: "1 Jan 1970")) - - // test view representation - XCTAssertEqual(filter.viewModel.description, "last activity is 1 Jan 1970") - - // test sql representation - XCTAssertEqual(renderSQL(filter.leftHandSide), #""last_activity_at""#) - XCTAssertEqual(renderSQL(filter.sqlOperator), "=") - XCTAssertEqual(binds(filter.rightHandSide), ["1970-01-01"]) - - // test error case - XCTAssertThrowsError(try LastActivitySearchFilter( - expression: .init(operator: .greaterThan, value: "23rd June 2021")) - ) { - XCTAssertEqual($0 as? SearchFilterError, .invalidValueType) + @Test func lastActivityFilter() async throws { + try await withApp { app in + let filter = try LastActivitySearchFilter(expression: .init(operator: .is, + value: "1970-01-01")) + #expect(filter.key == .lastActivity) + #expect(filter.predicate == .init(operator: .equal, + bindableValue: .value("1970-01-01"), + displayValue: "1 Jan 1970")) + + // test view representation + #expect(filter.viewModel.description == "last activity is 1 Jan 1970") + + // test sql representation + #expect(app.db.renderSQL(filter.leftHandSide) == #""last_activity_at""#) + #expect(app.db.renderSQL(filter.sqlOperator) == "=") + #expect(app.db.binds(filter.rightHandSide) == ["1970-01-01"]) + #expect { + try LastActivitySearchFilter(expression: .init(operator: .greaterThan, value: "23rd June 2021")) + } throws: { + $0 as? SearchFilterError == .invalidValueType + } } } - func test_lastCommitFilter() throws { - let filter = try LastCommitSearchFilter(expression: .init(operator: .is, - value: "1970-01-01")) - XCTAssertEqual(filter.key, .lastCommit) - XCTAssertEqual(filter.predicate, .init(operator: .equal, - bindableValue: .value("1970-01-01"), - displayValue: "1 Jan 1970")) - - // test view representation - XCTAssertEqual(filter.viewModel.description, "last commit is 1 Jan 1970") - - // test sql representation - XCTAssertEqual(renderSQL(filter.leftHandSide), #""last_commit_date""#) - XCTAssertEqual(renderSQL(filter.sqlOperator), "=") - XCTAssertEqual(binds(filter.rightHandSide), ["1970-01-01"]) - - // test error case - XCTAssertThrowsError(try LastCommitSearchFilter( - expression: .init(operator: .greaterThan, value: "23rd June 2021")) - ) { - XCTAssertEqual($0 as? SearchFilterError, .invalidValueType) + @Test func lastCommitFilter() async throws { + try await withApp { app in + let filter = try LastCommitSearchFilter(expression: .init(operator: .is, + value: "1970-01-01")) + #expect(filter.key == .lastCommit) + #expect(filter.predicate == .init(operator: .equal, + bindableValue: .value("1970-01-01"), + displayValue: "1 Jan 1970")) + + // test view representation + #expect(filter.viewModel.description == "last commit is 1 Jan 1970") + + // test sql representation + #expect(app.db.renderSQL(filter.leftHandSide) == #""last_commit_date""#) + #expect(app.db.renderSQL(filter.sqlOperator) == "=") + #expect(app.db.binds(filter.rightHandSide) == ["1970-01-01"]) + #expect { + try LastCommitSearchFilter(expression: .init(operator: .greaterThan, value: "23rd June 2021")) + } throws: { + $0 as? SearchFilterError == .invalidValueType + } } } - func test_licenseFilter_compatible() throws { - let filter = try LicenseSearchFilter(expression: .init(operator: .is, - value: "compatible")) - XCTAssertEqual(filter.key, .license) - XCTAssertEqual(filter.predicate, .init(operator: .in, - bindableValue: .array(["afl-3.0", "apache-2.0", "artistic-2.0", "bsd-2-clause", "bsd-3-clause", "bsd-3-clause-clear", "bsl-1.0", "cc", "cc0-1.0", "cc-by-4.0", "cc-by-sa-4.0", "wtfpl", "ecl-2.0", "epl-1.0", "eupl-1.1", "isc", "ms-pl", "mit", "mpl-2.0", "osl-3.0", "postgresql", "ncsa", "unlicense", "zlib"]), - displayValue: "compatible with the App Store")) + @Test func licenseFilter_compatible() async throws { + try await withApp { app in + let filter = try LicenseSearchFilter(expression: .init(operator: .is, + value: "compatible")) + #expect(filter.key == .license) + #expect(filter.predicate == .init(operator: .in, + bindableValue: .array(["afl-3.0", "apache-2.0", "artistic-2.0", "bsd-2-clause", "bsd-3-clause", "bsd-3-clause-clear", "bsl-1.0", "cc", "cc0-1.0", "cc-by-4.0", "cc-by-sa-4.0", "wtfpl", "ecl-2.0", "epl-1.0", "eupl-1.1", "isc", "ms-pl", "mit", "mpl-2.0", "osl-3.0", "postgresql", "ncsa", "unlicense", "zlib"]), + displayValue: "compatible with the App Store")) - // test view representation - XCTAssertEqual(filter.viewModel.description, "license is compatible with the App Store") + // test view representation + #expect(filter.viewModel.description == "license is compatible with the App Store") - // test sql representation - XCTAssertEqual(renderSQL(filter.leftHandSide), #""license""#) - XCTAssertEqual(renderSQL(filter.sqlOperator), "IN") - XCTAssertEqual(binds(filter.rightHandSide), ["afl-3.0", "apache-2.0", "artistic-2.0", "bsd-2-clause", "bsd-3-clause", "bsd-3-clause-clear", "bsl-1.0", "cc", "cc0-1.0", "cc-by-4.0", "cc-by-sa-4.0", "wtfpl", "ecl-2.0", "epl-1.0", "eupl-1.1", "isc", "ms-pl", "mit", "mpl-2.0", "osl-3.0", "postgresql", "ncsa", "unlicense", "zlib"]) + // test sql representation + #expect(app.db.renderSQL(filter.leftHandSide) == #""license""#) + #expect(app.db.renderSQL(filter.sqlOperator) == "IN") + #expect(app.db.binds(filter.rightHandSide) == ["afl-3.0", "apache-2.0", "artistic-2.0", "bsd-2-clause", "bsd-3-clause", "bsd-3-clause-clear", "bsl-1.0", "cc", "cc0-1.0", "cc-by-4.0", "cc-by-sa-4.0", "wtfpl", "ecl-2.0", "epl-1.0", "eupl-1.1", "isc", "ms-pl", "mit", "mpl-2.0", "osl-3.0", "postgresql", "ncsa", "unlicense", "zlib"]) + } } - func test_licenseFilter_single() throws { - let filter = try LicenseSearchFilter(expression: .init(operator: .is, - value: "mit")) - XCTAssertEqual(filter.key, .license) - XCTAssertEqual(filter.predicate, .init(operator: .in, - bindableValue: .array(["mit"]), - displayValue: "MIT")) + @Test func licenseFilter_single() async throws { + try await withApp { app in + let filter = try LicenseSearchFilter(expression: .init(operator: .is, + value: "mit")) + #expect(filter.key == .license) + #expect(filter.predicate == .init(operator: .in, + bindableValue: .array(["mit"]), + displayValue: "MIT")) - // test view representation - XCTAssertEqual(filter.viewModel.description, "license is MIT") + // test view representation + #expect(filter.viewModel.description == "license is MIT") - // test sql representation - XCTAssertEqual(renderSQL(filter.leftHandSide), #""license""#) - XCTAssertEqual(renderSQL(filter.sqlOperator), "IN") - XCTAssertEqual(binds(filter.rightHandSide), ["mit"]) + // test sql representation + #expect(app.db.renderSQL(filter.leftHandSide) == #""license""#) + #expect(app.db.renderSQL(filter.sqlOperator) == "IN") + #expect(app.db.binds(filter.rightHandSide) == ["mit"]) + } } - func test_licenseFilter_case_insensitive() throws { - XCTAssertEqual( + @Test func licenseFilter_case_insensitive() throws { + #expect( try LicenseSearchFilter( expression: .init(operator: .is, - value: "mit")).bindableValue, - ["mit"] + value: "mit")).bindableValue == ["mit"] ) - XCTAssertEqual( + #expect( try LicenseSearchFilter( expression: .init(operator: .is, - value: "MIT")).bindableValue, - ["mit"] + value: "MIT")).bindableValue == ["mit"] ) - XCTAssertEqual( + #expect( try LicenseSearchFilter( expression: .init(operator: .is, - value: "Compatible")).bindableValue, - ["afl-3.0", "apache-2.0", "artistic-2.0", "bsd-2-clause", "bsd-3-clause", "bsd-3-clause-clear", "bsl-1.0", "cc", "cc0-1.0", "cc-by-4.0", "cc-by-sa-4.0", "wtfpl", "ecl-2.0", "epl-1.0", "eupl-1.1", "isc", "ms-pl", "mit", "mpl-2.0", "osl-3.0", "postgresql", "ncsa", "unlicense", "zlib"] + value: "Compatible")).bindableValue == ["afl-3.0", "apache-2.0", "artistic-2.0", "bsd-2-clause", "bsd-3-clause", "bsd-3-clause-clear", "bsl-1.0", "cc", "cc0-1.0", "cc-by-4.0", "cc-by-sa-4.0", "wtfpl", "ecl-2.0", "epl-1.0", "eupl-1.1", "isc", "ms-pl", "mit", "mpl-2.0", "osl-3.0", "postgresql", "ncsa", "unlicense", "zlib"] ) } - func test_licenseFilter_incompatible() throws { - let filter = try LicenseSearchFilter(expression: .init(operator: .is, - value: "incompatible")) - XCTAssertEqual(filter.key, .license) - XCTAssertEqual(filter.predicate, .init(operator: .in, - bindableValue: .array(["agpl-3.0", "gpl", "gpl-2.0", "gpl-3.0", "lgpl", "lgpl-2.1", "lgpl-3.0"]), - displayValue: "incompatible with the App Store")) + @Test func licenseFilter_incompatible() async throws { + try await withApp { app in + let filter = try LicenseSearchFilter(expression: .init(operator: .is, + value: "incompatible")) + #expect(filter.key == .license) + #expect(filter.predicate == .init(operator: .in, + bindableValue: .array(["agpl-3.0", "gpl", "gpl-2.0", "gpl-3.0", "lgpl", "lgpl-2.1", "lgpl-3.0"]), + displayValue: "incompatible with the App Store")) - // test view representation - XCTAssertEqual(filter.viewModel.description, "license is incompatible with the App Store") + // test view representation + #expect(filter.viewModel.description == "license is incompatible with the App Store") - // test sql representation - XCTAssertEqual(renderSQL(filter.leftHandSide), #""license""#) - XCTAssertEqual(renderSQL(filter.sqlOperator), "IN") - XCTAssertEqual(binds(filter.rightHandSide), ["agpl-3.0", "gpl", "gpl-2.0", "gpl-3.0", "lgpl", "lgpl-2.1", "lgpl-3.0"]) + // test sql representation + #expect(app.db.renderSQL(filter.leftHandSide) == #""license""#) + #expect(app.db.renderSQL(filter.sqlOperator) == "IN") + #expect(app.db.binds(filter.rightHandSide) == ["agpl-3.0", "gpl", "gpl-2.0", "gpl-3.0", "lgpl", "lgpl-2.1", "lgpl-3.0"]) + } } - func test_licenseFilter_none() throws { - let filter = try LicenseSearchFilter(expression: .init(operator: .is, - value: "none")) - XCTAssertEqual(filter.key, .license) - XCTAssertEqual(filter.predicate, .init(operator: .in, - bindableValue: .array(["none"]), - displayValue: "not defined")) + @Test func licenseFilter_none() async throws { + try await withApp { app in + let filter = try LicenseSearchFilter(expression: .init(operator: .is, value: "none")) + #expect(filter.key == .license) + #expect(filter.predicate == .init(operator: .in, + bindableValue: .array(["none"]), + displayValue: "not defined")) - // test view representation - XCTAssertEqual(filter.viewModel.description, "license is not defined") + // test view representation + #expect(filter.viewModel.description == "license is not defined") - // test sql representation - XCTAssertEqual(renderSQL(filter.leftHandSide), #""license""#) - XCTAssertEqual(renderSQL(filter.sqlOperator), "IN") - XCTAssertEqual(binds(filter.rightHandSide), ["none"]) + // test sql representation + #expect(app.db.renderSQL(filter.leftHandSide) == #""license""#) + #expect(app.db.renderSQL(filter.sqlOperator) == "IN") + #expect(app.db.binds(filter.rightHandSide) == ["none"]) + } } - func test_licenseFilter_other() throws { - let filter = try LicenseSearchFilter(expression: .init(operator: .is, - value: "other")) - XCTAssertEqual(filter.key, .license) - XCTAssertEqual(filter.predicate, .init(operator: .in, - bindableValue: .array(["other"]), - displayValue: "unknown")) + @Test func licenseFilter_other() async throws { + try await withApp { app in + let filter = try LicenseSearchFilter(expression: .init(operator: .is, value: "other")) + #expect(filter.key == .license) + #expect(filter.predicate == .init(operator: .in, + bindableValue: .array(["other"]), + displayValue: "unknown")) - // test view representation - XCTAssertEqual(filter.viewModel.description, "license is unknown") + // test view representation + #expect(filter.viewModel.description == "license is unknown") - // test sql representation - XCTAssertEqual(renderSQL(filter.leftHandSide), #""license""#) - XCTAssertEqual(renderSQL(filter.sqlOperator), "IN") - XCTAssertEqual(binds(filter.rightHandSide), ["other"]) + // test sql representation + #expect(app.db.renderSQL(filter.leftHandSide) == #""license""#) + #expect(app.db.renderSQL(filter.sqlOperator) == "IN") + #expect(app.db.binds(filter.rightHandSide) == ["other"]) + } } - func test_licenseFilter_error() throws { - // test error case - XCTAssertThrowsError(try LicenseSearchFilter( - expression: .init(operator: .greaterThan, value: "mit")) - ) { - XCTAssertEqual($0 as? SearchFilterError, .unsupportedComparisonMethod) + @Test func licenseFilter_error() throws { + #expect { + try LicenseSearchFilter(expression: .init(operator: .greaterThan, value: "mit")) + } throws: { + $0 as? SearchFilterError == .unsupportedComparisonMethod } } - func test_platformFilter_single_value() throws { + @Test func platformFilter_single_value() async throws { // test single value happy path - let filter = try PlatformSearchFilter(expression: .init(operator: .is, - value: "ios")) - XCTAssertEqual(filter.key, .platform) - XCTAssertEqual(filter.predicate, .init(operator: .contains, - bindableValue: .value("ios"), - displayValue: "iOS")) - - // test view representation - XCTAssertEqual(filter.viewModel.description, "platform compatibility is iOS") - - // test sql representation - XCTAssertEqual(renderSQL(filter.leftHandSide), #""platform_compatibility""#) - XCTAssertEqual(renderSQL(filter.sqlOperator), "@>") - XCTAssertEqual(binds(filter.rightHandSide), ["{ios}"]) + try await withApp { app in + let filter = try PlatformSearchFilter(expression: .init(operator: .is, + value: "ios")) + #expect(filter.key == .platform) + #expect(filter.predicate == .init(operator: .contains, + bindableValue: .value("ios"), + displayValue: "iOS")) + + // test view representation + #expect(filter.viewModel.description == "platform compatibility is iOS") + + // test sql representation + #expect(app.db.renderSQL(filter.leftHandSide) == #""platform_compatibility""#) + #expect(app.db.renderSQL(filter.sqlOperator) == "@>") + #expect(app.db.binds(filter.rightHandSide) == ["{ios}"]) + } } - func test_platformFilter_case_insensitive() throws { - XCTAssertEqual( + @Test func platformFilter_case_insensitive() throws { + #expect( try PlatformSearchFilter(expression: .init(operator: .is, - value: "ios")).bindableValue, - [.iOS] + value: "ios")).bindableValue == [.iOS] ) - XCTAssertEqual( + #expect( try PlatformSearchFilter(expression: .init(operator: .is, - value: "iOS")).bindableValue, - [.iOS] + value: "iOS")).bindableValue == [.iOS] ) } - func test_platformFilter_deduplication() throws { + @Test func platformFilter_deduplication() throws { // test de-duplication and compact-mapping of invalid terms - XCTAssertEqual( + #expect( try PlatformSearchFilter(expression: .init(operator: .is, - value: "iOS,macos,MacOS X")).bindableValue, - [.iOS, .macOS] + value: "iOS,macos,MacOS X")).bindableValue == [.iOS, .macOS] ) - XCTAssertEqual( + #expect( try PlatformSearchFilter(expression: .init(operator: .is, - value: "iOS,macos,ios")).bindableValue, - [.iOS, .macOS] + value: "iOS,macos,ios")).bindableValue == [.iOS, .macOS] ) } - func test_platformFilter_multiple_values() throws { + @Test func platformFilter_multiple_values() throws { // test predicate with multiple values do { let predicate = try PlatformSearchFilter( expression: .init(operator: .is, value: "iOS,macos,ios")).predicate - XCTAssertEqual(predicate.bindableValue.asPlatforms, - [.iOS, .macOS]) - XCTAssertEqual(predicate.operator, .contains) + #expect(predicate.bindableValue.asPlatforms == [.iOS, .macOS]) + #expect(predicate.operator == .contains) } do { let predicate = try PlatformSearchFilter( expression: .init(operator: .is, value: "iOS,macos,linux")).predicate - XCTAssertEqual(predicate.bindableValue.asPlatforms, - [.iOS, .linux, .macOS]) - XCTAssertEqual(predicate.operator, .contains) + #expect(predicate.bindableValue.asPlatforms == [.iOS, .linux, .macOS]) + #expect(predicate.operator == .contains) } // test view representation with multiple values - XCTAssertEqual( + #expect( try PlatformSearchFilter(expression: .init(operator: .is, value: "iOS,macos,ios")) - .viewModel.description, - "platform compatibility is iOS and macOS" + .viewModel.description == "platform compatibility is iOS and macOS" ) - XCTAssertEqual( + #expect( try PlatformSearchFilter(expression: .init(operator: .is, value: "iOS,macos,linux")) - .viewModel.description, - "platform compatibility is iOS, Linux, and macOS" + .viewModel.description == "platform compatibility is iOS, Linux, and macOS" ) } - func test_platformFilter_error() throws { - // test error cases - XCTAssertThrowsError(try PlatformSearchFilter( - expression: .init(operator: .isNot, value: "foo")) - ) { - XCTAssertEqual($0 as? SearchFilterError, .unsupportedComparisonMethod) + @Test func platformFilter_error() throws { + #expect { + try PlatformSearchFilter(expression: .init(operator: .isNot, value: "foo")) + } throws: { + $0 as? SearchFilterError == .unsupportedComparisonMethod } for value in ["foo", "", ",", "MacOS X"] { - XCTAssertThrowsError(try PlatformSearchFilter( - expression: .init(operator: .is, value: value)) - ) { - XCTAssertEqual($0 as? SearchFilterError, .invalidValueType, "expected exception for value: \(value)") + #expect("expected exception for value: \(value)") { + try PlatformSearchFilter(expression: .init(operator: .is, value: value)) + } throws: { + $0 as? SearchFilterError == .invalidValueType } } } - func test_starsFilter() throws { - let filter = try StarsSearchFilter(expression: .init(operator: .is, value: "1234")) - XCTAssertEqual(filter.key, .stars) - XCTAssertEqual(filter.predicate, .init(operator: .equal, - bindableValue: .value("1234"), - displayValue: "1,234")) - - // test view representation - XCTAssertEqual(filter.viewModel.description, "stars is 1,234") - - // test sql representation - XCTAssertEqual(renderSQL(filter.leftHandSide), #""stars""#) - XCTAssertEqual(renderSQL(filter.sqlOperator), "=") - XCTAssertEqual(binds(filter.rightHandSide), ["1234"]) - - // test error case - XCTAssertThrowsError(try StarsSearchFilter( - expression: .init(operator: .greaterThan, value: "one")) - ) { - XCTAssertEqual($0 as? SearchFilterError, .invalidValueType) + @Test func starsFilter() async throws { + try await withApp { app in + let filter = try StarsSearchFilter(expression: .init(operator: .is, value: "1234")) + #expect(filter.key == .stars) + #expect(filter.predicate == .init(operator: .equal, + bindableValue: .value("1234"), + displayValue: "1,234")) + + // test view representation + #expect(filter.viewModel.description == "stars is 1,234") + + // test sql representation + #expect(app.db.renderSQL(filter.leftHandSide) == #""stars""#) + #expect(app.db.renderSQL(filter.sqlOperator) == "=") + #expect(app.db.binds(filter.rightHandSide) == ["1234"]) + #expect { + try StarsSearchFilter(expression: .init(operator: .greaterThan, value: "one")) + } throws: { + $0 as? SearchFilterError == .invalidValueType + } } } - func test_productTypeFilter() throws { - // test single value happy path - let filter = try ProductTypeSearchFilter(expression: .init(operator: .is, - value: "executable")) - XCTAssertEqual(filter.key, .productType) - XCTAssertEqual(filter.predicate, .init(operator: .contains, - bindableValue: .value("executable"), - displayValue: "Executable")) + @Test func productTypeFilter() async throws { + try await withApp { app in + // test single value happy path + let filter = try ProductTypeSearchFilter(expression: .init(operator: .is, + value: "executable")) + #expect(filter.key == .productType) + #expect(filter.predicate == .init(operator: .contains, + bindableValue: .value("executable"), + displayValue: "Executable")) - // test view representation - XCTAssertEqual(filter.viewModel.description, "Package products contain an Executable") + // test view representation + #expect(filter.viewModel.description == "Package products contain an Executable") - // test sql representation - XCTAssertEqual(renderSQL(filter.leftHandSide), #""product_types""#) - XCTAssertEqual(renderSQL(filter.sqlOperator), "@>") - XCTAssertEqual(binds(filter.rightHandSide), ["{executable}"]) + // test sql representation + #expect(app.db.renderSQL(filter.leftHandSide) == #""product_types""#) + #expect(app.db.renderSQL(filter.sqlOperator) == "@>") + #expect(app.db.binds(filter.rightHandSide) == ["{executable}"]) + } } - func test_productTypeFilter_macro() throws { - // Test "virtual" macro product filter - let filter = try ProductTypeSearchFilter(expression: .init(operator: .is, value: "macro")) - XCTAssertEqual(filter.key, .productType) - XCTAssertEqual(filter.predicate, .init(operator: .contains, - bindableValue: .value("macro"), - displayValue: "Macro")) + @Test func productTypeFilter_macro() async throws { + try await withApp { app in + // Test "virtual" macro product filter + let filter = try ProductTypeSearchFilter(expression: .init(operator: .is, value: "macro")) + #expect(filter.key == .productType) + #expect(filter.predicate == .init(operator: .contains, + bindableValue: .value("macro"), + displayValue: "Macro")) - // test view representation - XCTAssertEqual(filter.viewModel.description, "Package products contain a Macro") + // test view representation + #expect(filter.viewModel.description == "Package products contain a Macro") - // test sql representation - XCTAssertEqual(renderSQL(filter.leftHandSide), #""product_types""#) - XCTAssertEqual(renderSQL(filter.sqlOperator), "@>") - XCTAssertEqual(binds(filter.rightHandSide), ["{macro}"]) + // test sql representation + #expect(app.db.renderSQL(filter.leftHandSide) == #""product_types""#) + #expect(app.db.renderSQL(filter.sqlOperator) == "@>") + #expect(app.db.binds(filter.rightHandSide) == ["{macro}"]) + } } - func test_productTypeFilter_spelling() throws { + @Test func productTypeFilter_spelling() throws { let expectedDisplayValues = [ ProductTypeSearchFilter.ProductType.executable: "Package products contain an Executable", ProductTypeSearchFilter.ProductType.plugin: "Package products contain a Plugin", @@ -458,22 +453,21 @@ class SearchFilterTests: AppTestCase { for type in ProductTypeSearchFilter.ProductType.allCases { let filter = try ProductTypeSearchFilter(expression: .init(operator: .is, value: type.rawValue)) - XCTAssertEqual(filter.viewModel.description, expectedDisplayValues[type]) + #expect(filter.viewModel.description == expectedDisplayValues[type]) } } - func test_productTypeFilter_error() throws { - // test error cases - XCTAssertThrowsError(try ProductTypeSearchFilter( - expression: .init(operator: .isNot, value: "foo")) - ) { - XCTAssertEqual($0 as? SearchFilterError, .unsupportedComparisonMethod) + @Test func productTypeFilter_error() async throws { + #expect { + try ProductTypeSearchFilter(expression: .init(operator: .isNot, value: "foo")) + } throws: { + $0 as? SearchFilterError == .unsupportedComparisonMethod } for value in ["foo", "", ",", "MacOS X"] { - XCTAssertThrowsError(try ProductTypeSearchFilter( - expression: .init(operator: .is, value: value)) - ) { - XCTAssertEqual($0 as? SearchFilterError, .invalidValueType, "expected exception for value: \(value)") + #expect("expected exception for value: \(value)") { + try ProductTypeSearchFilter(expression: .init(operator: .is, value: value)) + } throws: { + $0 as? SearchFilterError == .invalidValueType } } } @@ -526,7 +520,7 @@ extension App.SearchFilter.Predicate: Swift.Equatable { extension App.SearchFilter.Predicate.BoundValue: Swift.Equatable { public static func == (lhs: SearchFilter.Predicate.BoundValue, rhs: SearchFilter.Predicate.BoundValue) -> Bool { - renderSQL(lhs.sqlBind) == renderSQL(rhs.sqlBind) + renderGenericSQL(lhs.sqlBind) == renderGenericSQL(rhs.sqlBind) } var asPlatforms: [Package.PlatformCompatibility]? { @@ -544,7 +538,7 @@ extension App.SearchFilter.Predicate.BoundValue: Swift.Equatable { // This renderSQL helper uses a dummy SQLDatabase dialect defined in `TestDatabase`. // It should only be used in cases where app.db (which is using the PostgresDB dialect) // is not available and where the exact syntax of SQL details is not relevant. -private func renderSQL(_ query: SQLExpression) -> String { +private func renderGenericSQL(_ query: SQLExpression) -> String { var serializer = SQLSerializer(database: TestDatabase()) query.serialize(to: &serializer) return serializer.sql From 0ebaab3f1668e84c71262c3e54babb5da979e9e5 Mon Sep 17 00:00:00 2001 From: "Sven A. Schmidt" Date: Tue, 4 Mar 2025 11:23:04 +0100 Subject: [PATCH 2/5] Convert SearchShowModelAppTests --- Tests/AppTests/SearchShowModelAppTests.swift | 27 +++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/Tests/AppTests/SearchShowModelAppTests.swift b/Tests/AppTests/SearchShowModelAppTests.swift index f883146e9..f087531be 100644 --- a/Tests/AppTests/SearchShowModelAppTests.swift +++ b/Tests/AppTests/SearchShowModelAppTests.swift @@ -16,25 +16,28 @@ import Dependencies import SwiftSoup -import XCTVapor +import Testing +import Vapor -class SearchShowModelAppTests: AppTestCase { +@Suite struct SearchShowModelAppTests { - func test_SearchShow_Model_canonicalURLAllowList() async throws { + @Test func SearchShow_Model_canonicalURLAllowList() async throws { try await withDependencies { $0.environment.dbId = { nil } } operation: { - let request = Vapor.Request(application: app, - url: "search?query=alamo&page=2&utm_campaign=test&utm_source=email", - on: app.eventLoopGroup.next()) - let html = try await SearchController.show(req: request).render() - let document = try SwiftSoup.parse(html) - let linkElements = try document.select("link[rel='canonical']") - XCTAssertEqual(linkElements.count, 1) + try await withApp { app in + let request = Vapor.Request(application: app, + url: "search?query=alamo&page=2&utm_campaign=test&utm_source=email", + on: app.eventLoopGroup.next()) + let html = try await SearchController.show(req: request).render() + let document = try SwiftSoup.parse(html) + let linkElements = try document.select("link[rel='canonical']") + #expect(linkElements.count == 1) - let href = try linkElements.first()!.attr("href") - XCTAssertEqual(href, "http://localhost:8080/search?query=alamo&page=2") + let href = try linkElements.first()!.attr("href") + #expect(href == "http://localhost:8080/search?query=alamo&page=2") + } } } From fe664729d9863cb25752a591a4ab4d9c22bf2d23 Mon Sep 17 00:00:00 2001 From: "Sven A. Schmidt" Date: Tue, 4 Mar 2025 11:24:03 +0100 Subject: [PATCH 3/5] Convert SearchShowModelTests --- Tests/AppTests/SearchShowModelTests.swift | 67 +++++++++++------------ 1 file changed, 33 insertions(+), 34 deletions(-) diff --git a/Tests/AppTests/SearchShowModelTests.swift b/Tests/AppTests/SearchShowModelTests.swift index cee1b71f0..10e23fb18 100644 --- a/Tests/AppTests/SearchShowModelTests.swift +++ b/Tests/AppTests/SearchShowModelTests.swift @@ -14,11 +14,12 @@ @testable import App -import XCTVapor +import Testing -class SearchShowModelTests: XCTestCase { - func test_SearchShow_Model_init() throws { +@Suite struct SearchShowModelTests { + + @Test func SearchShow_Model_init() throws { let results: [Search.Result] = .mock() // MUT @@ -31,20 +32,20 @@ class SearchShowModelTests: XCTestCase { ], results: results), weightedKeywords: []) - XCTAssertEqual(model.page, 1) - XCTAssertEqual(model.query, "query key:value") - XCTAssertEqual(model.term, "query") + #expect(model.page == 1) + #expect(model.query == "query key:value") + #expect(model.term == "query") - XCTAssertEqual(model.filters.count, 1) - XCTAssertEqual(model.filters[0].key, "key") - XCTAssertEqual(model.filters[0].operator, "is") - XCTAssertEqual(model.filters[0].value, "value") + #expect(model.filters.count == 1) + #expect(model.filters[0].key == "key") + #expect(model.filters[0].operator == "is") + #expect(model.filters[0].value == "value") - XCTAssertEqual(model.response.hasMoreResults, false) - XCTAssertEqual(model.response.results.count, 10) + #expect(model.response.hasMoreResults == false) + #expect(model.response.results.count == 10) } - func test_SearchShow_Model_init_sanitized() throws { + @Test func SearchShow_Model_init_sanitized() throws { do { // https://github.com/SwiftPackageIndex/SwiftPackageIndex-Server/issues/1409 let query = #"'>">"# @@ -55,13 +56,11 @@ class SearchShowModelTests: XCTestCase { weightedKeywords: [] ) - XCTAssertEqual( - model.query, - "'>"></script><svg/onload=confirm(42)>" + #expect( + model.query == "'>"></script><svg/onload=confirm(42)>" ) - XCTAssertEqual( - model.term, - "'>"></script><svg/onload=confirm(42)>" + #expect( + model.term == "'>"></script><svg/onload=confirm(42)>" ) } do { @@ -74,12 +73,12 @@ class SearchShowModelTests: XCTestCase { weightedKeywords: [] ) - XCTAssertEqual(model.query, "test'two") - XCTAssertEqual(model.term, "test'two") + #expect(model.query == "test'two") + #expect(model.term == "test'two") } } - func test_SearchShow_Model_authorResults() throws { + @Test func SearchShow_Model_authorResults() throws { let results: [Search.Result] = .mock() let model = SearchShow.Model(query: .init(query: "query", page: 1), response: .init(hasMoreResults: false, @@ -91,10 +90,10 @@ class SearchShowModelTests: XCTestCase { // MUT let authorResult = model.authorResults.first! - XCTAssertEqual(authorResult.name, "Apple") + #expect(authorResult.name == "Apple") } - func test_SearchShow_Model_keywordResults() throws { + @Test func SearchShow_Model_keywordResults() throws { let results: [Search.Result] = .mock() let model = SearchShow.Model(query: .init(query: "query", page:1 ), response: .init(hasMoreResults: false, @@ -106,10 +105,10 @@ class SearchShowModelTests: XCTestCase { // MUT let keywordResult = model.keywordResults.first! - XCTAssertEqual(keywordResult.keyword, "keyword1") + #expect(keywordResult.keyword == "keyword1") } - func test_SearchShow_Model_packageResults() throws { + @Test func SearchShow_Model_packageResults() throws { let results: [Search.Result] = .mock() let model = SearchShow.Model(query: .init(query: "query", page: 1), response: .init(hasMoreResults: false, @@ -121,15 +120,15 @@ class SearchShowModelTests: XCTestCase { // MUT let packageResult = model.packageResults.first! - XCTAssertEqual(packageResult.packageId, .id1) - XCTAssertEqual(packageResult.packageName, "Package One") - XCTAssertEqual(packageResult.packageURL, "https://example.com/package/one") - XCTAssertEqual(packageResult.repositoryName, "one") - XCTAssertEqual(packageResult.repositoryOwner, "package") - XCTAssertEqual(packageResult.summary, "This is a package filled with ones.") + #expect(packageResult.packageId == .id1) + #expect(packageResult.packageName == "Package One") + #expect(packageResult.packageURL == "https://example.com/package/one") + #expect(packageResult.repositoryName == "one") + #expect(packageResult.repositoryOwner == "package") + #expect(packageResult.summary == "This is a package filled with ones.") } - func test_SearchShow_Model_matchingKeywords() throws { + @Test func SearchShow_Model_matchingKeywords() throws { let results: [Search.Result] = .mock() let model = SearchShow.Model(query: .init(query: "query", page: 1), response: .init(hasMoreResults: false, @@ -141,7 +140,7 @@ class SearchShowModelTests: XCTestCase { // MUT let matchingKeywords = model.matchingKeywords(packageKeywords: ["keyword2", "keyword4", "keyword5"]) - XCTAssertEqual(matchingKeywords, ["keyword2", "keyword4"]) + #expect(matchingKeywords == ["keyword2", "keyword4"]) } } From e983ce9666b221441f2c4b7ce90a3a9b37fbe80b Mon Sep 17 00:00:00 2001 From: "Sven A. Schmidt" Date: Tue, 4 Mar 2025 11:54:51 +0100 Subject: [PATCH 4/5] Convert SearchTests --- Tests/AppTests/AppTestCase.swift | 8 + Tests/AppTests/SearchTests.swift | 2611 +++++++++++++++--------------- 2 files changed, 1351 insertions(+), 1268 deletions(-) diff --git a/Tests/AppTests/AppTestCase.swift b/Tests/AppTests/AppTestCase.swift index 9afeb7d66..9a9328db1 100644 --- a/Tests/AppTests/AppTestCase.swift +++ b/Tests/AppTests/AppTestCase.swift @@ -184,12 +184,20 @@ extension AppTestCase { // FIXME: Move this once AppTestCase can be removed. These are helpers created during the transition to Swift Testing. extension Database { + func renderSQL(_ builder: SQLSelectBuilder) -> String { + renderSQL(builder.query) + } + func renderSQL(_ query: SQLExpression) -> String { var serializer = SQLSerializer(database: self as! SQLDatabase) query.serialize(to: &serializer) return serializer.sql } + func binds(_ builder: SQLSelectBuilder?) -> [String] { + binds(builder?.query) + } + func binds(_ query: SQLExpression?) -> [String] { var serializer = SQLSerializer(database: self as! SQLDatabase) query?.serialize(to: &serializer) diff --git a/Tests/AppTests/SearchTests.swift b/Tests/AppTests/SearchTests.swift index 6662ace23..eb934021f 100644 --- a/Tests/AppTests/SearchTests.swift +++ b/Tests/AppTests/SearchTests.swift @@ -12,1521 +12,1596 @@ // See the License for the specific language governing permissions and // limitations under the License. +import Foundation + @testable import App -import SnapshotTesting import SQLKit -import XCTVapor +import SnapshotTesting +import Testing -class SearchTests: AppTestCase { +@Suite struct SearchTests { - func test_DBRecord_packageURL() throws { - XCTAssertEqual(Search.DBRecord(matchType: .package, + @Test func DBRecord_packageURL() async throws { + #expect(Search.DBRecord(matchType: .package, packageId: UUID(), repositoryName: "bar", repositoryOwner: "foo", - hasDocs: false).packageURL, - "/foo/bar") - XCTAssertEqual(Search.DBRecord(matchType: .package, + hasDocs: false).packageURL == "/foo/bar") + #expect(Search.DBRecord(matchType: .package, packageId: UUID(), repositoryName: "foo bar", repositoryOwner: "baz", - hasDocs: false).packageURL, - "/baz/foo%20bar") + hasDocs: false).packageURL == "/baz/foo%20bar") } - func test_packageMatchQuery_single_term() throws { - let b = Search.packageMatchQueryBuilder(on: app.db, terms: ["a"], filters: []) - XCTAssertEqual(renderSQL(b), #"SELECT 'package' AS "match_type", NULL AS "keyword", "package_id", "package_name", "repo_name", "repo_owner", "score", "summary", "stars", "license", "last_commit_date", "last_activity_at", "keywords", "has_docs", NULL::INT AS "levenshtein_dist", ts_rank("tsvector", "tsquery") >= 0.05 AS "has_exact_word_matches" FROM "search", plainto_tsquery($1) AS "tsquery" WHERE CONCAT_WS(' ', "package_name", COALESCE("summary", ''), "repo_name", "repo_owner", ARRAY_TO_STRING("keywords", ' '), ARRAY_TO_STRING("product_names", ' ')) ~* $2 AND "repo_owner" IS NOT NULL AND "repo_name" IS NOT NULL ORDER BY LOWER(COALESCE("package_name", '')) = $3 DESC, "has_exact_word_matches" DESC, "score" DESC, "stars" DESC, "package_name" ASC"#) - XCTAssertEqual(binds(b), ["a", "a", "a"]) + @Test func packageMatchQuery_single_term() async throws { + try await withApp { app in + let b = Search.packageMatchQueryBuilder(on: app.db, terms: ["a"], filters: []) + #expect(app.db.renderSQL(b) == #"SELECT 'package' AS "match_type", NULL AS "keyword", "package_id", "package_name", "repo_name", "repo_owner", "score", "summary", "stars", "license", "last_commit_date", "last_activity_at", "keywords", "has_docs", NULL::INT AS "levenshtein_dist", ts_rank("tsvector", "tsquery") >= 0.05 AS "has_exact_word_matches" FROM "search", plainto_tsquery($1) AS "tsquery" WHERE CONCAT_WS(' ', "package_name", COALESCE("summary", ''), "repo_name", "repo_owner", ARRAY_TO_STRING("keywords", ' '), ARRAY_TO_STRING("product_names", ' ')) ~* $2 AND "repo_owner" IS NOT NULL AND "repo_name" IS NOT NULL ORDER BY LOWER(COALESCE("package_name", '')) = $3 DESC, "has_exact_word_matches" DESC, "score" DESC, "stars" DESC, "package_name" ASC"#) + #expect(app.db.binds(b) == ["a", "a", "a"]) + } } - func test_packageMatchQuery_multiple_terms() throws { - let b = Search.packageMatchQueryBuilder(on: app.db, terms: ["a", "b"], filters: []) - XCTAssertEqual(renderSQL(b), #"SELECT 'package' AS "match_type", NULL AS "keyword", "package_id", "package_name", "repo_name", "repo_owner", "score", "summary", "stars", "license", "last_commit_date", "last_activity_at", "keywords", "has_docs", NULL::INT AS "levenshtein_dist", ts_rank("tsvector", "tsquery") >= 0.05 AS "has_exact_word_matches" FROM "search", plainto_tsquery($1) AS "tsquery" WHERE CONCAT_WS(' ', "package_name", COALESCE("summary", ''), "repo_name", "repo_owner", ARRAY_TO_STRING("keywords", ' '), ARRAY_TO_STRING("product_names", ' ')) ~* $2 AND CONCAT_WS(' ', "package_name", COALESCE("summary", ''), "repo_name", "repo_owner", ARRAY_TO_STRING("keywords", ' '), ARRAY_TO_STRING("product_names", ' ')) ~* $3 AND "repo_owner" IS NOT NULL AND "repo_name" IS NOT NULL ORDER BY LOWER(COALESCE("package_name", '')) = $4 DESC, "has_exact_word_matches" DESC, "score" DESC, "stars" DESC, "package_name" ASC"#) - XCTAssertEqual(binds(b), ["a b", "a", "b", "a b"]) + @Test func packageMatchQuery_multiple_terms() async throws { + try await withApp { app in + let b = Search.packageMatchQueryBuilder(on: app.db, terms: ["a", "b"], filters: []) + #expect(app.db.renderSQL(b) == #"SELECT 'package' AS "match_type", NULL AS "keyword", "package_id", "package_name", "repo_name", "repo_owner", "score", "summary", "stars", "license", "last_commit_date", "last_activity_at", "keywords", "has_docs", NULL::INT AS "levenshtein_dist", ts_rank("tsvector", "tsquery") >= 0.05 AS "has_exact_word_matches" FROM "search", plainto_tsquery($1) AS "tsquery" WHERE CONCAT_WS(' ', "package_name", COALESCE("summary", ''), "repo_name", "repo_owner", ARRAY_TO_STRING("keywords", ' '), ARRAY_TO_STRING("product_names", ' ')) ~* $2 AND CONCAT_WS(' ', "package_name", COALESCE("summary", ''), "repo_name", "repo_owner", ARRAY_TO_STRING("keywords", ' '), ARRAY_TO_STRING("product_names", ' ')) ~* $3 AND "repo_owner" IS NOT NULL AND "repo_name" IS NOT NULL ORDER BY LOWER(COALESCE("package_name", '')) = $4 DESC, "has_exact_word_matches" DESC, "score" DESC, "stars" DESC, "package_name" ASC"#) + #expect(app.db.binds(b) == ["a b", "a", "b", "a b"]) + } } - func test_packageMatchQuery_AuthorSearchFilter() throws { - let b = Search.packageMatchQueryBuilder( - on: app.db, terms: ["a"], - filters: [try AuthorSearchFilter(expression: .init(operator: .is, value: "foo"))] - ) + @Test func packageMatchQuery_AuthorSearchFilter() async throws { + try await withApp { app in + let b = Search.packageMatchQueryBuilder( + on: app.db, terms: ["a"], + filters: [try AuthorSearchFilter(expression: .init(operator: .is, value: "foo"))] + ) - XCTAssertEqual(renderSQL(b), """ + #expect(app.db.renderSQL(b) == """ SELECT 'package' AS "match_type", NULL AS "keyword", "package_id", "package_name", "repo_name", "repo_owner", "score", "summary", "stars", "license", "last_commit_date", "last_activity_at", "keywords", "has_docs", NULL::INT AS "levenshtein_dist", ts_rank("tsvector", "tsquery") >= 0.05 AS "has_exact_word_matches" FROM "search", plainto_tsquery($1) AS "tsquery" WHERE CONCAT_WS(' ', "package_name", COALESCE("summary", ''), "repo_name", "repo_owner", ARRAY_TO_STRING("keywords", ' '), ARRAY_TO_STRING("product_names", ' ')) ~* $2 AND "repo_owner" IS NOT NULL AND "repo_name" IS NOT NULL AND ("repo_owner" ILIKE $3) ORDER BY LOWER(COALESCE("package_name", '')) = $4 DESC, "has_exact_word_matches" DESC, "score" DESC, "stars" DESC, "package_name" ASC """) - XCTAssertEqual(binds(b), ["a", "a", "foo", "a"]) - } - - func test_packageMatchQuery_KeywordSearchFilter() throws { - let b = Search.packageMatchQueryBuilder( - on: app.db, terms: ["a"], - filters: [try KeywordSearchFilter(expression: .init(operator: .is, - value: "foo"))] - ) - - XCTAssertEqual(renderSQL(b), """ - SELECT 'package' AS "match_type", NULL AS "keyword", "package_id", "package_name", "repo_name", "repo_owner", "score", "summary", "stars", "license", "last_commit_date", "last_activity_at", "keywords", "has_docs", NULL::INT AS "levenshtein_dist", ts_rank("tsvector", "tsquery") >= 0.05 AS "has_exact_word_matches" FROM "search", plainto_tsquery($1) AS "tsquery" WHERE CONCAT_WS(' ', "package_name", COALESCE("summary", ''), "repo_name", "repo_owner", ARRAY_TO_STRING("keywords", ' '), ARRAY_TO_STRING("product_names", ' ')) ~* $2 AND "repo_owner" IS NOT NULL AND "repo_name" IS NOT NULL AND ($3 ILIKE ANY("keywords")) ORDER BY LOWER(COALESCE("package_name", '')) = $4 DESC, "has_exact_word_matches" DESC, "score" DESC, "stars" DESC, "package_name" ASC - """) - XCTAssertEqual(binds(b), ["a", "a", "foo", "a"]) + #expect(app.db.binds(b) == ["a", "a", "foo", "a"]) + } } - func test_packageMatchQuery_LastActivitySearchFilter() throws { - let b = Search.packageMatchQueryBuilder( - on: app.db, terms: ["a"], - filters: [try LastActivitySearchFilter(expression: .init(operator: .greaterThan, - value: "2021-12-01"))] - ) + @Test func packageMatchQuery_KeywordSearchFilter() async throws { + try await withApp { app in + let b = Search.packageMatchQueryBuilder( + on: app.db, terms: ["a"], + filters: [try KeywordSearchFilter(expression: .init(operator: .is, + value: "foo"))] + ) - XCTAssertEqual(renderSQL(b), """ - SELECT 'package' AS "match_type", NULL AS "keyword", "package_id", "package_name", "repo_name", "repo_owner", "score", "summary", "stars", "license", "last_commit_date", "last_activity_at", "keywords", "has_docs", NULL::INT AS "levenshtein_dist", ts_rank("tsvector", "tsquery") >= 0.05 AS "has_exact_word_matches" FROM "search", plainto_tsquery($1) AS "tsquery" WHERE CONCAT_WS(' ', "package_name", COALESCE("summary", ''), "repo_name", "repo_owner", ARRAY_TO_STRING("keywords", ' '), ARRAY_TO_STRING("product_names", ' ')) ~* $2 AND "repo_owner" IS NOT NULL AND "repo_name" IS NOT NULL AND ("last_activity_at" > $3) ORDER BY LOWER(COALESCE("package_name", '')) = $4 DESC, "has_exact_word_matches" DESC, "score" DESC, "stars" DESC, "package_name" ASC - """) - XCTAssertEqual(binds(b), ["a", "a", "2021-12-01", "a"]) + #expect(app.db.renderSQL(b) == """ + SELECT 'package' AS "match_type", NULL AS "keyword", "package_id", "package_name", "repo_name", "repo_owner", "score", "summary", "stars", "license", "last_commit_date", "last_activity_at", "keywords", "has_docs", NULL::INT AS "levenshtein_dist", ts_rank("tsvector", "tsquery") >= 0.05 AS "has_exact_word_matches" FROM "search", plainto_tsquery($1) AS "tsquery" WHERE CONCAT_WS(' ', "package_name", COALESCE("summary", ''), "repo_name", "repo_owner", ARRAY_TO_STRING("keywords", ' '), ARRAY_TO_STRING("product_names", ' ')) ~* $2 AND "repo_owner" IS NOT NULL AND "repo_name" IS NOT NULL AND ($3 ILIKE ANY("keywords")) ORDER BY LOWER(COALESCE("package_name", '')) = $4 DESC, "has_exact_word_matches" DESC, "score" DESC, "stars" DESC, "package_name" ASC + """) + #expect(app.db.binds(b) == ["a", "a", "foo", "a"]) + } } - func test_packageMatchQuery_LastCommitSearchFilter() throws { - let b = Search.packageMatchQueryBuilder( - on: app.db, terms: ["a"], - filters: [try LastCommitSearchFilter(expression: .init(operator: .greaterThan, - value: "2021-12-01"))] - ) + @Test func packageMatchQuery_LastActivitySearchFilter() async throws { + try await withApp { app in + let b = Search.packageMatchQueryBuilder( + on: app.db, terms: ["a"], + filters: [try LastActivitySearchFilter(expression: .init(operator: .greaterThan, + value: "2021-12-01"))] + ) - XCTAssertEqual(renderSQL(b), """ - SELECT 'package' AS "match_type", NULL AS "keyword", "package_id", "package_name", "repo_name", "repo_owner", "score", "summary", "stars", "license", "last_commit_date", "last_activity_at", "keywords", "has_docs", NULL::INT AS "levenshtein_dist", ts_rank("tsvector", "tsquery") >= 0.05 AS "has_exact_word_matches" FROM "search", plainto_tsquery($1) AS "tsquery" WHERE CONCAT_WS(' ', "package_name", COALESCE("summary", ''), "repo_name", "repo_owner", ARRAY_TO_STRING("keywords", ' '), ARRAY_TO_STRING("product_names", ' ')) ~* $2 AND "repo_owner" IS NOT NULL AND "repo_name" IS NOT NULL AND ("last_commit_date" > $3) ORDER BY LOWER(COALESCE("package_name", '')) = $4 DESC, "has_exact_word_matches" DESC, "score" DESC, "stars" DESC, "package_name" ASC - """) - XCTAssertEqual(binds(b), ["a", "a", "2021-12-01", "a"]) + #expect(app.db.renderSQL(b) == """ + SELECT 'package' AS "match_type", NULL AS "keyword", "package_id", "package_name", "repo_name", "repo_owner", "score", "summary", "stars", "license", "last_commit_date", "last_activity_at", "keywords", "has_docs", NULL::INT AS "levenshtein_dist", ts_rank("tsvector", "tsquery") >= 0.05 AS "has_exact_word_matches" FROM "search", plainto_tsquery($1) AS "tsquery" WHERE CONCAT_WS(' ', "package_name", COALESCE("summary", ''), "repo_name", "repo_owner", ARRAY_TO_STRING("keywords", ' '), ARRAY_TO_STRING("product_names", ' ')) ~* $2 AND "repo_owner" IS NOT NULL AND "repo_name" IS NOT NULL AND ("last_activity_at" > $3) ORDER BY LOWER(COALESCE("package_name", '')) = $4 DESC, "has_exact_word_matches" DESC, "score" DESC, "stars" DESC, "package_name" ASC + """) + #expect(app.db.binds(b) == ["a", "a", "2021-12-01", "a"]) + } } - func test_packageMatchQuery_LicenseSearchFilter() throws { - let b = Search.packageMatchQueryBuilder( - on: app.db, terms: ["a"], - filters: [try LicenseSearchFilter(expression: .init(operator: .is, value: "mit"))] - ) + @Test func packageMatchQuery_LastCommitSearchFilter() async throws { + try await withApp { app in + let b = Search.packageMatchQueryBuilder( + on: app.db, terms: ["a"], + filters: [try LastCommitSearchFilter(expression: .init(operator: .greaterThan, + value: "2021-12-01"))] + ) - XCTAssertEqual(renderSQL(b), """ - SELECT 'package' AS "match_type", NULL AS "keyword", "package_id", "package_name", "repo_name", "repo_owner", "score", "summary", "stars", "license", "last_commit_date", "last_activity_at", "keywords", "has_docs", NULL::INT AS "levenshtein_dist", ts_rank("tsvector", "tsquery") >= 0.05 AS "has_exact_word_matches" FROM "search", plainto_tsquery($1) AS "tsquery" WHERE CONCAT_WS(' ', "package_name", COALESCE("summary", ''), "repo_name", "repo_owner", ARRAY_TO_STRING("keywords", ' '), ARRAY_TO_STRING("product_names", ' ')) ~* $2 AND "repo_owner" IS NOT NULL AND "repo_name" IS NOT NULL AND ("license" IN ($3)) ORDER BY LOWER(COALESCE("package_name", '')) = $4 DESC, "has_exact_word_matches" DESC, "score" DESC, "stars" DESC, "package_name" ASC - """) - XCTAssertEqual(binds(b), ["a", "a", "mit", "a"]) + #expect(app.db.renderSQL(b) == """ + SELECT 'package' AS "match_type", NULL AS "keyword", "package_id", "package_name", "repo_name", "repo_owner", "score", "summary", "stars", "license", "last_commit_date", "last_activity_at", "keywords", "has_docs", NULL::INT AS "levenshtein_dist", ts_rank("tsvector", "tsquery") >= 0.05 AS "has_exact_word_matches" FROM "search", plainto_tsquery($1) AS "tsquery" WHERE CONCAT_WS(' ', "package_name", COALESCE("summary", ''), "repo_name", "repo_owner", ARRAY_TO_STRING("keywords", ' '), ARRAY_TO_STRING("product_names", ' ')) ~* $2 AND "repo_owner" IS NOT NULL AND "repo_name" IS NOT NULL AND ("last_commit_date" > $3) ORDER BY LOWER(COALESCE("package_name", '')) = $4 DESC, "has_exact_word_matches" DESC, "score" DESC, "stars" DESC, "package_name" ASC + """) + #expect(app.db.binds(b) == ["a", "a", "2021-12-01", "a"]) + } } - func test_packageMatchQuery_PlatformSearchFilter() throws { - let b = Search.packageMatchQueryBuilder( - on: app.db, terms: ["a"], - filters: [try PlatformSearchFilter(expression: .init(operator: .is, value: "ios,macos"))] - ) + @Test func packageMatchQuery_LicenseSearchFilter() async throws { + try await withApp { app in + let b = Search.packageMatchQueryBuilder( + on: app.db, terms: ["a"], + filters: [try LicenseSearchFilter(expression: .init(operator: .is, value: "mit"))] + ) - XCTAssertEqual(renderSQL(b), """ - SELECT 'package' AS "match_type", NULL AS "keyword", "package_id", "package_name", "repo_name", "repo_owner", "score", "summary", "stars", "license", "last_commit_date", "last_activity_at", "keywords", "has_docs", NULL::INT AS "levenshtein_dist", ts_rank("tsvector", "tsquery") >= 0.05 AS "has_exact_word_matches" FROM "search", plainto_tsquery($1) AS "tsquery" WHERE CONCAT_WS(' ', "package_name", COALESCE("summary", ''), "repo_name", "repo_owner", ARRAY_TO_STRING("keywords", ' '), ARRAY_TO_STRING("product_names", ' ')) ~* $2 AND "repo_owner" IS NOT NULL AND "repo_name" IS NOT NULL AND ("platform_compatibility" @> $3) ORDER BY LOWER(COALESCE("package_name", '')) = $4 DESC, "has_exact_word_matches" DESC, "score" DESC, "stars" DESC, "package_name" ASC - """) - XCTAssertEqual(binds(b), ["a", "a", "{ios,macos}", "a"]) + #expect(app.db.renderSQL(b) == """ + SELECT 'package' AS "match_type", NULL AS "keyword", "package_id", "package_name", "repo_name", "repo_owner", "score", "summary", "stars", "license", "last_commit_date", "last_activity_at", "keywords", "has_docs", NULL::INT AS "levenshtein_dist", ts_rank("tsvector", "tsquery") >= 0.05 AS "has_exact_word_matches" FROM "search", plainto_tsquery($1) AS "tsquery" WHERE CONCAT_WS(' ', "package_name", COALESCE("summary", ''), "repo_name", "repo_owner", ARRAY_TO_STRING("keywords", ' '), ARRAY_TO_STRING("product_names", ' ')) ~* $2 AND "repo_owner" IS NOT NULL AND "repo_name" IS NOT NULL AND ("license" IN ($3)) ORDER BY LOWER(COALESCE("package_name", '')) = $4 DESC, "has_exact_word_matches" DESC, "score" DESC, "stars" DESC, "package_name" ASC + """) + #expect(app.db.binds(b) == ["a", "a", "mit", "a"]) + } } - func test_packageMatchQuery_ProductTypeSearchFilter() throws { - for type in ProductTypeSearchFilter.ProductType.allCases { + @Test func packageMatchQuery_PlatformSearchFilter() async throws { + try await withApp { app in let b = Search.packageMatchQueryBuilder( on: app.db, terms: ["a"], - filters: [ - try ProductTypeSearchFilter(expression: .init(operator: .is, value: type.rawValue)) - ] + filters: [try PlatformSearchFilter(expression: .init(operator: .is, value: "ios,macos"))] ) - XCTAssertEqual(renderSQL(b), """ - SELECT 'package' AS "match_type", NULL AS "keyword", "package_id", "package_name", "repo_name", "repo_owner", "score", "summary", "stars", "license", "last_commit_date", "last_activity_at", "keywords", "has_docs", NULL::INT AS "levenshtein_dist", ts_rank("tsvector", "tsquery") >= 0.05 AS "has_exact_word_matches" FROM "search", plainto_tsquery($1) AS "tsquery" WHERE CONCAT_WS(' ', "package_name", COALESCE("summary", ''), "repo_name", "repo_owner", ARRAY_TO_STRING("keywords", ' '), ARRAY_TO_STRING("product_names", ' ')) ~* $2 AND "repo_owner" IS NOT NULL AND "repo_name" IS NOT NULL AND ("product_types" @> $3) ORDER BY LOWER(COALESCE("package_name", '')) = $4 DESC, "has_exact_word_matches" DESC, "score" DESC, "stars" DESC, "package_name" ASC - """) - XCTAssertEqual(binds(b), ["a", "a", "{\(type.rawValue)}", "a"]) + + #expect(app.db.renderSQL(b) == """ + SELECT 'package' AS "match_type", NULL AS "keyword", "package_id", "package_name", "repo_name", "repo_owner", "score", "summary", "stars", "license", "last_commit_date", "last_activity_at", "keywords", "has_docs", NULL::INT AS "levenshtein_dist", ts_rank("tsvector", "tsquery") >= 0.05 AS "has_exact_word_matches" FROM "search", plainto_tsquery($1) AS "tsquery" WHERE CONCAT_WS(' ', "package_name", COALESCE("summary", ''), "repo_name", "repo_owner", ARRAY_TO_STRING("keywords", ' '), ARRAY_TO_STRING("product_names", ' ')) ~* $2 AND "repo_owner" IS NOT NULL AND "repo_name" IS NOT NULL AND ("platform_compatibility" @> $3) ORDER BY LOWER(COALESCE("package_name", '')) = $4 DESC, "has_exact_word_matches" DESC, "score" DESC, "stars" DESC, "package_name" ASC + """) + #expect(app.db.binds(b) == ["a", "a", "{ios,macos}", "a"]) } } - func test_packageMatchQuery_StarsSearchFilter() throws { - let b = Search.packageMatchQueryBuilder( - on: app.db, terms: ["a"], - filters: [try StarsSearchFilter(expression: .init(operator: .greaterThan, - value: "500"))]) + @Test func packageMatchQuery_ProductTypeSearchFilter() async throws { + try await withApp { app in + for type in ProductTypeSearchFilter.ProductType.allCases { + let b = Search.packageMatchQueryBuilder( + on: app.db, terms: ["a"], + filters: [ + try ProductTypeSearchFilter(expression: .init(operator: .is, value: type.rawValue)) + ] + ) + #expect(app.db.renderSQL(b) == """ + SELECT 'package' AS "match_type", NULL AS "keyword", "package_id", "package_name", "repo_name", "repo_owner", "score", "summary", "stars", "license", "last_commit_date", "last_activity_at", "keywords", "has_docs", NULL::INT AS "levenshtein_dist", ts_rank("tsvector", "tsquery") >= 0.05 AS "has_exact_word_matches" FROM "search", plainto_tsquery($1) AS "tsquery" WHERE CONCAT_WS(' ', "package_name", COALESCE("summary", ''), "repo_name", "repo_owner", ARRAY_TO_STRING("keywords", ' '), ARRAY_TO_STRING("product_names", ' ')) ~* $2 AND "repo_owner" IS NOT NULL AND "repo_name" IS NOT NULL AND ("product_types" @> $3) ORDER BY LOWER(COALESCE("package_name", '')) = $4 DESC, "has_exact_word_matches" DESC, "score" DESC, "stars" DESC, "package_name" ASC + """) + #expect(app.db.binds(b) == ["a", "a", "{\(type.rawValue)}", "a"]) + } + } + } - XCTAssertEqual(renderSQL(b), """ - SELECT 'package' AS "match_type", NULL AS "keyword", "package_id", "package_name", "repo_name", "repo_owner", "score", "summary", "stars", "license", "last_commit_date", "last_activity_at", "keywords", "has_docs", NULL::INT AS "levenshtein_dist", ts_rank("tsvector", "tsquery") >= 0.05 AS "has_exact_word_matches" FROM "search", plainto_tsquery($1) AS "tsquery" WHERE CONCAT_WS(' ', "package_name", COALESCE("summary", ''), "repo_name", "repo_owner", ARRAY_TO_STRING("keywords", ' '), ARRAY_TO_STRING("product_names", ' ')) ~* $2 AND "repo_owner" IS NOT NULL AND "repo_name" IS NOT NULL AND ("stars" > $3) ORDER BY LOWER(COALESCE("package_name", '')) = $4 DESC, "has_exact_word_matches" DESC, "score" DESC, "stars" DESC, "package_name" ASC - """) - XCTAssertEqual(binds(b), ["a", "a", "500", "a"]) + @Test func packageMatchQuery_StarsSearchFilter() async throws { + try await withApp { app in + let b = Search.packageMatchQueryBuilder( + on: app.db, terms: ["a"], + filters: [try StarsSearchFilter(expression: .init(operator: .greaterThan, + value: "500"))]) + + #expect(app.db.renderSQL(b) == """ + SELECT 'package' AS "match_type", NULL AS "keyword", "package_id", "package_name", "repo_name", "repo_owner", "score", "summary", "stars", "license", "last_commit_date", "last_activity_at", "keywords", "has_docs", NULL::INT AS "levenshtein_dist", ts_rank("tsvector", "tsquery") >= 0.05 AS "has_exact_word_matches" FROM "search", plainto_tsquery($1) AS "tsquery" WHERE CONCAT_WS(' ', "package_name", COALESCE("summary", ''), "repo_name", "repo_owner", ARRAY_TO_STRING("keywords", ' '), ARRAY_TO_STRING("product_names", ' ')) ~* $2 AND "repo_owner" IS NOT NULL AND "repo_name" IS NOT NULL AND ("stars" > $3) ORDER BY LOWER(COALESCE("package_name", '')) = $4 DESC, "has_exact_word_matches" DESC, "score" DESC, "stars" DESC, "package_name" ASC + """) + #expect(app.db.binds(b) == ["a", "a", "500", "a"]) + } } - func test_keywordMatchQuery_single_term() throws { - let b = Search.keywordMatchQueryBuilder(on: app.db, terms: ["a"]) - XCTAssertEqual(renderSQL(b), #"SELECT DISTINCT 'keyword' AS "match_type", "keyword", NULL::UUID AS "package_id", NULL AS "package_name", NULL AS "repo_name", NULL AS "repo_owner", NULL::INT AS "score", NULL AS "summary", NULL::INT AS "stars", NULL AS "license", NULL::TIMESTAMP AS "last_commit_date", NULL::TIMESTAMP AS "last_activity_at", NULL::TEXT[] AS "keywords", NULL::BOOL AS "has_docs", NULL::INT AS "levenshtein_dist", NULL::BOOL AS "has_exact_word_matches" FROM "search", UNNEST("keywords") AS "keyword" WHERE "keyword" ILIKE $1 LIMIT 50"#) - XCTAssertEqual(binds(b), ["%a%"]) + @Test func keywordMatchQuery_single_term() async throws { + try await withApp { app in + let b = Search.keywordMatchQueryBuilder(on: app.db, terms: ["a"]) + #expect(app.db.renderSQL(b) == #"SELECT DISTINCT 'keyword' AS "match_type", "keyword", NULL::UUID AS "package_id", NULL AS "package_name", NULL AS "repo_name", NULL AS "repo_owner", NULL::INT AS "score", NULL AS "summary", NULL::INT AS "stars", NULL AS "license", NULL::TIMESTAMP AS "last_commit_date", NULL::TIMESTAMP AS "last_activity_at", NULL::TEXT[] AS "keywords", NULL::BOOL AS "has_docs", NULL::INT AS "levenshtein_dist", NULL::BOOL AS "has_exact_word_matches" FROM "search", UNNEST("keywords") AS "keyword" WHERE "keyword" ILIKE $1 LIMIT 50"#) + #expect(app.db.binds(b) == ["%a%"]) + } } - func test_keywordMatchQuery_multiple_terms() throws { - let b = Search.keywordMatchQueryBuilder(on: app.db, terms: ["a", "b"]) - XCTAssertEqual(renderSQL(b), #"SELECT DISTINCT 'keyword' AS "match_type", "keyword", NULL::UUID AS "package_id", NULL AS "package_name", NULL AS "repo_name", NULL AS "repo_owner", NULL::INT AS "score", NULL AS "summary", NULL::INT AS "stars", NULL AS "license", NULL::TIMESTAMP AS "last_commit_date", NULL::TIMESTAMP AS "last_activity_at", NULL::TEXT[] AS "keywords", NULL::BOOL AS "has_docs", NULL::INT AS "levenshtein_dist", NULL::BOOL AS "has_exact_word_matches" FROM "search", UNNEST("keywords") AS "keyword" WHERE "keyword" ILIKE $1 LIMIT 50"#) - XCTAssertEqual(binds(b), ["%a b%"]) + @Test func keywordMatchQuery_multiple_terms() async throws { + try await withApp { app in + let b = Search.keywordMatchQueryBuilder(on: app.db, terms: ["a", "b"]) + #expect(app.db.renderSQL(b) == #"SELECT DISTINCT 'keyword' AS "match_type", "keyword", NULL::UUID AS "package_id", NULL AS "package_name", NULL AS "repo_name", NULL AS "repo_owner", NULL::INT AS "score", NULL AS "summary", NULL::INT AS "stars", NULL AS "license", NULL::TIMESTAMP AS "last_commit_date", NULL::TIMESTAMP AS "last_activity_at", NULL::TEXT[] AS "keywords", NULL::BOOL AS "has_docs", NULL::INT AS "levenshtein_dist", NULL::BOOL AS "has_exact_word_matches" FROM "search", UNNEST("keywords") AS "keyword" WHERE "keyword" ILIKE $1 LIMIT 50"#) + #expect(app.db.binds(b) == ["%a b%"]) + } } - func test_authorMatchQuery_single_term() throws { - let b = Search.authorMatchQueryBuilder(on: app.db, terms: ["a"]) - XCTAssertEqual(renderSQL(b), #"SELECT DISTINCT 'author' AS "match_type", NULL AS "keyword", NULL::UUID AS "package_id", NULL AS "package_name", NULL AS "repo_name", "repo_owner", NULL::INT AS "score", NULL AS "summary", NULL::INT AS "stars", NULL AS "license", NULL::TIMESTAMP AS "last_commit_date", NULL::TIMESTAMP AS "last_activity_at", NULL::TEXT[] AS "keywords", NULL::BOOL AS "has_docs", LEVENSHTEIN("repo_owner", $1) AS "levenshtein_dist", NULL::BOOL AS "has_exact_word_matches" FROM "search" WHERE "repo_owner" ILIKE $2 ORDER BY "levenshtein_dist" LIMIT 50"#) - XCTAssertEqual(binds(b), ["a", "%a%"]) + @Test func authorMatchQuery_single_term() async throws { + try await withApp { app in + let b = Search.authorMatchQueryBuilder(on: app.db, terms: ["a"]) + #expect(app.db.renderSQL(b) == #"SELECT DISTINCT 'author' AS "match_type", NULL AS "keyword", NULL::UUID AS "package_id", NULL AS "package_name", NULL AS "repo_name", "repo_owner", NULL::INT AS "score", NULL AS "summary", NULL::INT AS "stars", NULL AS "license", NULL::TIMESTAMP AS "last_commit_date", NULL::TIMESTAMP AS "last_activity_at", NULL::TEXT[] AS "keywords", NULL::BOOL AS "has_docs", LEVENSHTEIN("repo_owner", $1) AS "levenshtein_dist", NULL::BOOL AS "has_exact_word_matches" FROM "search" WHERE "repo_owner" ILIKE $2 ORDER BY "levenshtein_dist" LIMIT 50"#) + #expect(app.db.binds(b) == ["a", "%a%"]) + } } - func test_authorMatchQuery_multiple_term() throws { - let b = Search.authorMatchQueryBuilder(on: app.db, terms: ["a", "b"]) - XCTAssertEqual(renderSQL(b), #"SELECT DISTINCT 'author' AS "match_type", NULL AS "keyword", NULL::UUID AS "package_id", NULL AS "package_name", NULL AS "repo_name", "repo_owner", NULL::INT AS "score", NULL AS "summary", NULL::INT AS "stars", NULL AS "license", NULL::TIMESTAMP AS "last_commit_date", NULL::TIMESTAMP AS "last_activity_at", NULL::TEXT[] AS "keywords", NULL::BOOL AS "has_docs", LEVENSHTEIN("repo_owner", $1) AS "levenshtein_dist", NULL::BOOL AS "has_exact_word_matches" FROM "search" WHERE "repo_owner" ILIKE $2 ORDER BY "levenshtein_dist" LIMIT 50"#) - XCTAssertEqual(binds(b), ["a b", "%a b%"]) + @Test func authorMatchQuery_multiple_term() async throws { + try await withApp { app in + let b = Search.authorMatchQueryBuilder(on: app.db, terms: ["a", "b"]) + #expect(app.db.renderSQL(b) == #"SELECT DISTINCT 'author' AS "match_type", NULL AS "keyword", NULL::UUID AS "package_id", NULL AS "package_name", NULL AS "repo_name", "repo_owner", NULL::INT AS "score", NULL AS "summary", NULL::INT AS "stars", NULL AS "license", NULL::TIMESTAMP AS "last_commit_date", NULL::TIMESTAMP AS "last_activity_at", NULL::TEXT[] AS "keywords", NULL::BOOL AS "has_docs", LEVENSHTEIN("repo_owner", $1) AS "levenshtein_dist", NULL::BOOL AS "has_exact_word_matches" FROM "search" WHERE "repo_owner" ILIKE $2 ORDER BY "levenshtein_dist" LIMIT 50"#) + #expect(app.db.binds(b) == ["a b", "%a b%"]) + } } - func test_query_sql() throws { + @Test func query_sql() async throws { // Test to confirm shape of rendered search SQL - // MUT - let query = try XCTUnwrap(Search.query(app.db, ["test"], page: 1, pageSize: 20)) - // validate - XCTAssertEqual(renderSQL(query), """ + try await withApp { app in + // MUT + let query = try #require(Search.query(app.db, ["test"], page: 1, pageSize: 20)) + // validate + #expect(app.db.renderSQL(query) == """ SELECT * FROM ((SELECT DISTINCT 'author' AS "match_type", NULL AS "keyword", NULL::UUID AS "package_id", NULL AS "package_name", NULL AS "repo_name", "repo_owner", NULL::INT AS "score", NULL AS "summary", NULL::INT AS "stars", NULL AS "license", NULL::TIMESTAMP AS "last_commit_date", NULL::TIMESTAMP AS "last_activity_at", NULL::TEXT[] AS "keywords", NULL::BOOL AS "has_docs", LEVENSHTEIN("repo_owner", $1) AS "levenshtein_dist", NULL::BOOL AS "has_exact_word_matches" FROM "search" WHERE "repo_owner" ILIKE $2 ORDER BY "levenshtein_dist" LIMIT 50) UNION ALL (SELECT DISTINCT 'keyword' AS "match_type", "keyword", NULL::UUID AS "package_id", NULL AS "package_name", NULL AS "repo_name", NULL AS "repo_owner", NULL::INT AS "score", NULL AS "summary", NULL::INT AS "stars", NULL AS "license", NULL::TIMESTAMP AS "last_commit_date", NULL::TIMESTAMP AS "last_activity_at", NULL::TEXT[] AS "keywords", NULL::BOOL AS "has_docs", NULL::INT AS "levenshtein_dist", NULL::BOOL AS "has_exact_word_matches" FROM "search", UNNEST("keywords") AS "keyword" WHERE "keyword" ILIKE $3 LIMIT 50) UNION ALL (SELECT 'package' AS "match_type", NULL AS "keyword", "package_id", "package_name", "repo_name", "repo_owner", "score", "summary", "stars", "license", "last_commit_date", "last_activity_at", "keywords", "has_docs", NULL::INT AS "levenshtein_dist", ts_rank("tsvector", "tsquery") >= 0.05 AS "has_exact_word_matches" FROM "search", plainto_tsquery($4) AS "tsquery" WHERE CONCAT_WS(' ', "package_name", COALESCE("summary", ''), "repo_name", "repo_owner", ARRAY_TO_STRING("keywords", ' '), ARRAY_TO_STRING("product_names", ' ')) ~* $5 AND "repo_owner" IS NOT NULL AND "repo_name" IS NOT NULL ORDER BY LOWER(COALESCE("package_name", '')) = $6 DESC, "has_exact_word_matches" DESC, "score" DESC, "stars" DESC, "package_name" ASC LIMIT 21 OFFSET 0)) AS "t" """) - XCTAssertEqual(binds(query), ["test", "%test%", "%test%", "test", "test", "test"]) + #expect(app.db.binds(query) == ["test", "%test%", "%test%", "test", "test", "test"]) + } } - func test_fetch_single() async throws { + @Test func fetch_single() async throws { // Test search with a single term - // setup - let p1 = try await savePackage(on: app.db, "1") - let p2 = try await savePackage(on: app.db, "2") - try await Repository(package: p1, - defaultBranch: "main", - summary: "some package").save(on: app.db) - try await Repository(package: p2, - defaultBranch: "main", - lastCommitDate: .t0, - name: "name 2", - owner: "owner 2", - stars: 1234, - summary: "bar package").save(on: app.db) - try await Version(package: p1, packageName: "Foo", reference: .branch("main")).save(on: app.db) - try await Version(package: p2, packageName: "Bar", reference: .branch("main")).save(on: app.db) - try await Search.refresh(on: app.db) - - // MUT - let res = try await Search.fetch(app.db, ["bar"], page: 1, pageSize: 20) - - // validation - XCTAssertEqual(res, - .init(hasMoreResults: false, - searchTerm: "bar", - searchFilters: [], - results: [ - .package( - .init(packageId: try p2.requireID(), - packageName: "Bar", - packageURL: "/owner%202/name%202", - repositoryName: "name 2", - repositoryOwner: "owner 2", - stars: 1234, - lastActivityAt: .t0, - summary: "bar package", - keywords: [], - hasDocs: false)! - ) - ]) - ) - } - - func test_fetch_multiple() async throws { - // Test search with multiple terms ("and") - // setup - let p1 = try await savePackage(on: app.db, "1") - let p2 = try await savePackage(on: app.db, "2") - try await Repository(package: p1, - defaultBranch: "main", - name: "package 1", - owner: "owner", - summary: "package 1 description").save(on: app.db) - try await Repository(package: p2, - defaultBranch: "main", - lastCommitDate: .t0, - name: "package 2", - owner: "owner", - stars: 1234, - summary: "package 2 description").save(on: app.db) - try await Version(package: p1, packageName: "Foo", reference: .branch("main")).save(on: app.db) - try await Version(package: p2, packageName: "Bar", reference: .branch("main")).save(on: app.db) - try await Search.refresh(on: app.db) - - // MUT - let res = try await Search.fetch(app.db, ["owner", "bar"], page: 1, pageSize: 20) - - // validation - XCTAssertEqual(res, - .init(hasMoreResults: false, - searchTerm: "owner bar", - searchFilters: [], - results: [ - .package( - .init(packageId: try p2.requireID(), - packageName: "Bar", - packageURL: "/owner/package%202", - repositoryName: "package 2", - repositoryOwner: "owner", - stars: 1234, - lastActivityAt: .t0, - summary: "package 2 description", - keywords: [], - hasDocs: false)! - ) - ]) - ) - } + try await withApp { app in + // setup + let p1 = try await savePackage(on: app.db, id: .id1, "1") + let p2 = try await savePackage(on: app.db, id: .id2, "2") + try await Repository(package: p1, + defaultBranch: "main", + summary: "some package").save(on: app.db) + try await Repository(package: p2, + defaultBranch: "main", + lastCommitDate: .t0, + name: "name 2", + owner: "owner 2", + stars: 1234, + summary: "bar package").save(on: app.db) + try await Version(package: p1, packageName: "Foo", reference: .branch("main")).save(on: app.db) + try await Version(package: p2, packageName: "Bar", reference: .branch("main")).save(on: app.db) + try await Search.refresh(on: app.db) - func test_fetch_distinct() async throws { - // Ensure we de-duplicate results - // setup - let p = Package.init(id: .id0, url: "bar".url) - try await p.save(on: app.db) - try await Repository(package: p, - defaultBranch: "main", - name: "bar", - owner: "foo").save(on: app.db) - let v = try Version(package: p) - try await v.save(on: app.db) - try await Product(version: v, type: .library(.automatic), name: "lib").save(on: app.db) - try await Product(version: v, type: .plugin, name: "plugin").save(on: app.db) - try await Search.refresh(on: app.db) - - // MUT - let res = try await Search.fetch(app.db, ["bar"], page: 1, pageSize: 20) - - // validate - XCTAssertEqual(res.results.count, 1) - XCTAssertEqual(res.results.compactMap(\.packageResult?.repositoryName), ["bar"]) - } + // MUT + let res = try await Search.fetch(app.db, ["bar"], page: 1, pageSize: 20) - func test_quoting() async throws { - // Test searching for a `'` - // setup - let p1 = try await savePackage(on: app.db, "1") - let p2 = try await savePackage(on: app.db, "2") - try await Repository(package: p1, - defaultBranch: "main", - lastCommitDate: .t0, - name: "name 1", - owner: "owner 1", - stars: 1234, - summary: "some 'package'").save(on: app.db) - try await Repository(package: p2, - defaultBranch: "main", - lastCommitDate: .t0, - name: "name 2", - owner: "owner 2", - summary: "bar package").save(on: app.db) - try await Version(package: p1, packageName: "Foo", reference: .branch("main")).save(on: app.db) - try await Version(package: p2, packageName: "Bar", reference: .branch("main")).save(on: app.db) - try await Search.refresh(on: app.db) - - // MUT - let res = try await Search.fetch(app.db, ["'"], page: 1, pageSize: 20) - - // validation - XCTAssertEqual(res, - .init(hasMoreResults: false, - searchTerm: "'", - searchFilters: [], - results: [ - .package( - .init(packageId: try p1.requireID(), - packageName: "Foo", - packageURL: "/owner%201/name%201", - repositoryName: "name 1", - repositoryOwner: "owner 1", - stars: 1234, - lastActivityAt: .t0, - summary: "some 'package'", - keywords: [], - hasDocs: false)! - ) - ]) - ) + // validation + #expect(res == .init(hasMoreResults: false, + searchTerm: "bar", + searchFilters: [], + results: [ + .package( + .init(packageId: .id2, + packageName: "Bar", + packageURL: "/owner%202/name%202", + repositoryName: "name 2", + repositoryOwner: "owner 2", + stars: 1234, + lastActivityAt: .t0, + summary: "bar package", + keywords: [], + hasDocs: false)! + ) + ]) + ) + } } - func test_search_pagination() async throws { - // setup - let packages = (0..<9).map { idx in - Package(url: "\(idx)".url, score: 15 - idx) - } - try await packages.save(on: app.db) - try await packages.map { try Repository(package: $0, defaultBranch: "default", - name: $0.url, owner: "foobar") } - .save(on: app.db) - try await packages.map { try Version(package: $0, packageName: $0.url, reference: .branch("default")) } - .save(on: app.db) - try await Search.refresh(on: app.db) + @Test func fetch_multiple() async throws { + // Test search with multiple terms ("and") + try await withApp { app in + // setup + let p1 = try await savePackage(on: app.db, id: .id1, "1") + let p2 = try await savePackage(on: app.db, id: .id2, "2") + try await Repository(package: p1, + defaultBranch: "main", + name: "package 1", + owner: "owner", + summary: "package 1 description").save(on: app.db) + try await Repository(package: p2, + defaultBranch: "main", + lastCommitDate: .t0, + name: "package 2", + owner: "owner", + stars: 1234, + summary: "package 2 description").save(on: app.db) + try await Version(package: p1, packageName: "Foo", reference: .branch("main")).save(on: app.db) + try await Version(package: p2, packageName: "Bar", reference: .branch("main")).save(on: app.db) + try await Search.refresh(on: app.db) - do { // first page // MUT - let res = try await API.search(database: app.db, - query: .init(query: "foo", page: 1, pageSize: 3)) + let res = try await Search.fetch(app.db, ["owner", "bar"], page: 1, pageSize: 20) - // validate - XCTAssertTrue(res.hasMoreResults) - XCTAssertEqual(res.results.map(\.testDescription), - ["a:foobar", "p:0", "p:1", "p:2"]) + // validation + #expect(res == .init(hasMoreResults: false, + searchTerm: "owner bar", + searchFilters: [], + results: [ + .package( + .init(packageId: .id2, + packageName: "Bar", + packageURL: "/owner/package%202", + repositoryName: "package 2", + repositoryOwner: "owner", + stars: 1234, + lastActivityAt: .t0, + summary: "package 2 description", + keywords: [], + hasDocs: false)! + ) + ]) + ) } + } + + @Test func fetch_distinct() async throws { + // Ensure we de-duplicate results + try await withApp { app in + // setup + let p = Package.init(id: .id0, url: "bar".url) + try await p.save(on: app.db) + try await Repository(package: p, + defaultBranch: "main", + name: "bar", + owner: "foo").save(on: app.db) + let v = try Version(package: p) + try await v.save(on: app.db) + try await Product(version: v, type: .library(.automatic), name: "lib").save(on: app.db) + try await Product(version: v, type: .plugin, name: "plugin").save(on: app.db) + try await Search.refresh(on: app.db) - do { // second page // MUT - let res = try await API.search(database: app.db, - query: .init(query: "foo", page: 2, pageSize: 3)) + let res = try await Search.fetch(app.db, ["bar"], page: 1, pageSize: 20) // validate - XCTAssertTrue(res.hasMoreResults) - XCTAssertEqual(res.results.map(\.testDescription), - ["p:3", "p:4", "p:5"]) + #expect(res.results.count == 1) + #expect(res.results.compactMap(\.packageResult?.repositoryName) == ["bar"]) } + } + + @Test func quoting() async throws { + // Test searching for a `'` + try await withApp { app in + // setup + let p1 = try await savePackage(on: app.db, id: .id1, "1") + let p2 = try await savePackage(on: app.db, id: .id2, "2") + try await Repository(package: p1, + defaultBranch: "main", + lastCommitDate: .t0, + name: "name 1", + owner: "owner 1", + stars: 1234, + summary: "some 'package'").save(on: app.db) + try await Repository(package: p2, + defaultBranch: "main", + lastCommitDate: .t0, + name: "name 2", + owner: "owner 2", + summary: "bar package").save(on: app.db) + try await Version(package: p1, packageName: "Foo", reference: .branch("main")).save(on: app.db) + try await Version(package: p2, packageName: "Bar", reference: .branch("main")).save(on: app.db) + try await Search.refresh(on: app.db) - do { // third page // MUT - let res = try await API.search(database: app.db, - query: .init(query: "foo", page: 3, pageSize: 3)) + let res = try await Search.fetch(app.db, ["'"], page: 1, pageSize: 20) - // validate - XCTAssertFalse(res.hasMoreResults) - XCTAssertEqual(res.results.map(\.testDescription), - ["p:6", "p:7", "p:8"]) + // validation + #expect(res == .init(hasMoreResults: false, + searchTerm: "'", + searchFilters: [], + results: [ + .package( + .init(packageId: .id1, + packageName: "Foo", + packageURL: "/owner%201/name%201", + repositoryName: "name 1", + repositoryOwner: "owner 1", + stars: 1234, + lastActivityAt: .t0, + summary: "some 'package'", + keywords: [], + hasDocs: false)! + ) + ]) + ) } } - func test_search_pagination_with_author_keyword_results() async throws { - // https://github.com/SwiftPackageIndex/SwiftPackageIndex-Server/issues/1198 + @Test func search_pagination() async throws { // setup - let packages = (0..<9).map { idx in - Package(url: "\(idx)".url, score: 15 - idx) - } - try await packages.save(on: app.db) - try await packages.map { try Repository(package: $0, - defaultBranch: "default", - keywords: ["foo"], - name: $0.url, - owner: "foobar") } + try await withApp { app in + let packages = (0..<9).map { idx in + Package(url: "\(idx)".url, score: 15 - idx) + } + try await packages.save(on: app.db) + try await packages.map { try Repository(package: $0, defaultBranch: "default", + name: $0.url, owner: "foobar") } .save(on: app.db) - try await packages.map { try Version(package: $0, packageName: $0.url, reference: .branch("default")) } - .save(on: app.db) - try await Search.refresh(on: app.db) - - do { // first page - // MUT - let res = try await API.search(database: app.db, - query: .init(query: "foo", page: 1, pageSize: 3)) - - // validate - XCTAssertTrue(res.hasMoreResults) - XCTAssertEqual(res.results.map(\.testDescription), - ["a:foobar", "k:foo", "p:0", "p:1", "p:2"]) + try await packages.map { try Version(package: $0, packageName: $0.url, reference: .branch("default")) } + .save(on: app.db) + try await Search.refresh(on: app.db) + + do { // first page + // MUT + let res = try await API.search(database: app.db, + query: .init(query: "foo", page: 1, pageSize: 3)) + + // validate + #expect(res.hasMoreResults) + #expect(res.results.map(\.testDescription) == ["a:foobar", "p:0", "p:1", "p:2"]) + } + + do { // second page + // MUT + let res = try await API.search(database: app.db, + query: .init(query: "foo", page: 2, pageSize: 3)) + + // validate + #expect(res.hasMoreResults) + #expect(res.results.map(\.testDescription) == ["p:3", "p:4", "p:5"]) + } + + do { // third page + // MUT + let res = try await API.search(database: app.db, + query: .init(query: "foo", page: 3, pageSize: 3)) + + // validate + #expect(!res.hasMoreResults) + #expect(res.results.map(\.testDescription) == ["p:6", "p:7", "p:8"]) + } } + } - do { // second page - // MUT - let res = try await API.search(database: app.db, - query: .init(query: "foo", page: 2, pageSize: 3)) - - // validate - XCTAssertTrue(res.hasMoreResults) - XCTAssertEqual(res.results.map(\.testDescription), - ["p:3", "p:4", "p:5"]) + @Test func search_pagination_with_author_keyword_results() async throws { + // https://github.com/SwiftPackageIndex/SwiftPackageIndex-Server/issues/1198 + try await withApp { app in + // setup + let packages = (0..<9).map { idx in + Package(url: "\(idx)".url, score: 15 - idx) + } + try await packages.save(on: app.db) + try await packages.map { try Repository(package: $0, + defaultBranch: "default", + keywords: ["foo"], + name: $0.url, + owner: "foobar") } + .save(on: app.db) + try await packages.map { try Version(package: $0, packageName: $0.url, reference: .branch("default")) } + .save(on: app.db) + try await Search.refresh(on: app.db) + + do { // first page + // MUT + let res = try await API.search(database: app.db, + query: .init(query: "foo", page: 1, pageSize: 3)) + + // validate + #expect(res.hasMoreResults) + #expect(res.results.map(\.testDescription) == ["a:foobar", "k:foo", "p:0", "p:1", "p:2"]) + } + + do { // second page + // MUT + let res = try await API.search(database: app.db, + query: .init(query: "foo", page: 2, pageSize: 3)) + + // validate + #expect(res.hasMoreResults) + #expect(res.results.map(\.testDescription) == ["p:3", "p:4", "p:5"]) + } } } - func test_search_pagination_invalid_input() async throws { + @Test func search_pagination_invalid_input() async throws { // Test invalid pagination inputs - // setup - let packages = (0..<9).map { idx in - Package(url: "\(idx)".url, score: 15 - idx) - } - try await packages.save(on: app.db) - try await packages.map { try Repository(package: $0, defaultBranch: "default", - name: $0.url, owner: "foobar") } - .save(on: app.db) - - try await packages.map { try Version(package: $0, packageName: $0.url, reference: .branch("default")) } + try await withApp { app in + // setup + let packages = (0..<9).map { idx in + Package(url: "\(idx)".url, score: 15 - idx) + } + try await packages.save(on: app.db) + try await packages.map { try Repository(package: $0, defaultBranch: "default", + name: $0.url, owner: "foobar") } .save(on: app.db) - try await Search.refresh(on: app.db) - - do { // page out of bounds (too large) - // MUT - let res = try await API.search(database: app.db, - query: .init(query: "foo", page: 4, pageSize: 3)) + try await packages.map { try Version(package: $0, packageName: $0.url, reference: .branch("default")) } + .save(on: app.db) - // validate - XCTAssertFalse(res.hasMoreResults) - XCTAssertEqual(res.results.map(\.package?.repositoryName), - []) - } + try await Search.refresh(on: app.db) + + do { // page out of bounds (too large) + // MUT + let res = try await API.search(database: app.db, + query: .init(query: "foo", page: 4, pageSize: 3)) - do { // page out of bounds (too small - will be clamped to page 1) - // MUT - let res = try await API.search(database: app.db, - query: .init(query: "foo", page: 0, pageSize: 3)) - XCTAssertTrue(res.hasMoreResults) - XCTAssertEqual(res.results.map(\.testDescription), - ["a:foobar", "p:0", "p:1", "p:2"]) + // validate + #expect(!res.hasMoreResults) + #expect(res.results.map(\.package?.repositoryName) == []) + } + + do { // page out of bounds (too small - will be clamped to page 1) + // MUT + let res = try await API.search(database: app.db, + query: .init(query: "foo", page: 0, pageSize: 3)) + #expect(res.hasMoreResults) + #expect(res.results.map(\.testDescription) == ["a:foobar", "p:0", "p:1", "p:2"]) + } } } - func test_order_by_score() async throws { - // setup - for idx in (0..<10).shuffled() { - let p = Package(id: UUID(), url: "\(idx)".url, score: idx) - try await p.save(on: app.db) - try await Repository(package: p, - defaultBranch: "main", - name: "\(idx)", - owner: "foobar", - summary: "\(idx)").save(on: app.db) - try await Version(package: p, packageName: "\(idx)", reference: .branch("main")).save(on: app.db) - } - try await Search.refresh(on: app.db) + @Test func order_by_score() async throws { + try await withApp { app in + // setup + for idx in (0..<10).shuffled() { + let p = Package(id: UUID(), url: "\(idx)".url, score: idx) + try await p.save(on: app.db) + try await Repository(package: p, + defaultBranch: "main", + name: "\(idx)", + owner: "foobar", + summary: "\(idx)").save(on: app.db) + try await Version(package: p, packageName: "\(idx)", reference: .branch("main")).save(on: app.db) + } + try await Search.refresh(on: app.db) - // MUT - let res = try await Search.fetch(app.db, ["foo"], page: 1, pageSize: 20) + // MUT + let res = try await Search.fetch(app.db, ["foo"], page: 1, pageSize: 20) - // validation - XCTAssertEqual(res.results.count, 11) - XCTAssertEqual(res.results.map(\.testDescription), - ["a:foobar", "p:9", "p:8", "p:7", "p:6", "p:5", "p:4", "p:3", "p:2", "p:1", "p:0"]) + // validation + #expect(res.results.count == 11) + #expect(res.results.map(\.testDescription) == ["a:foobar", "p:9", "p:8", "p:7", "p:6", "p:5", "p:4", "p:3", "p:2", "p:1", "p:0"]) + } } - func test_exact_name_match() async throws { + @Test func exact_name_match() async throws { // Ensure exact name matches are boosted - // setup - // We have three packages that all match in some way: - // 1: exact package name match - we want this one to be at the top - // 2: package name contains search term - // 3: summary contains search term - let p1 = Package(id: UUID(), url: "1", score: 10) - let p2 = Package(id: UUID(), url: "2", score: 20) - let p3 = Package(id: UUID(), url: "3", score: 30) - try await [p1, p2, p3].save(on: app.db) - try await Repository(package: p1, - defaultBranch: "main", - name: "1", - owner: "foo", - summary: "").save(on: app.db) - try await Repository(package: p2, - defaultBranch: "main", - name: "2", - owner: "foo", - summary: "").save(on: app.db) - try await Repository(package: p3, - defaultBranch: "main", - name: "3", - owner: "foo", - summary: "link").save(on: app.db) - try await Version(package: p1, packageName: "Ink", reference: .branch("main")) - .save(on: app.db) - try await Version(package: p2, packageName: "inkInName", reference: .branch("main")) - .save(on: app.db) - try await Version(package: p3, packageName: "some name", reference: .branch("main")) - .save(on: app.db) - try await Search.refresh(on: app.db) + try await withApp { app in + // setup + // We have three packages that all match in some way: + // 1: exact package name match - we want this one to be at the top + // 2: package name contains search term + // 3: summary contains search term + let p1 = Package(id: UUID(), url: "1", score: 10) + let p2 = Package(id: UUID(), url: "2", score: 20) + let p3 = Package(id: UUID(), url: "3", score: 30) + try await [p1, p2, p3].save(on: app.db) + try await Repository(package: p1, + defaultBranch: "main", + name: "1", + owner: "foo", + summary: "").save(on: app.db) + try await Repository(package: p2, + defaultBranch: "main", + name: "2", + owner: "foo", + summary: "").save(on: app.db) + try await Repository(package: p3, + defaultBranch: "main", + name: "3", + owner: "foo", + summary: "link").save(on: app.db) + try await Version(package: p1, packageName: "Ink", reference: .branch("main")) + .save(on: app.db) + try await Version(package: p2, packageName: "inkInName", reference: .branch("main")) + .save(on: app.db) + try await Version(package: p3, packageName: "some name", reference: .branch("main")) + .save(on: app.db) + try await Search.refresh(on: app.db) - // MUT - let res = try await Search.fetch(app.db, ["ink"], page: 1, pageSize: 20) + // MUT + let res = try await Search.fetch(app.db, ["ink"], page: 1, pageSize: 20) - XCTAssertEqual(res.results.map(\.package?.repositoryName), ["1", "3", "2"]) + #expect(res.results.map(\.package?.repositoryName) == ["1", "3", "2"]) + } } - func test_exact_name_match_whitespace() async throws { + @Test func exact_name_match_whitespace() async throws { // Ensure exact name matches are boosted, for package name with whitespace - // setup - // We have three packages that all match in some way: - // 1: exact package name match - we want this one to be at the top - // 2: package name contains search term - // 3: summary contains search term - let p1 = Package(id: UUID(), url: "1", score: 10) - let p2 = Package(id: UUID(), url: "2", score: 20) - let p3 = Package(id: UUID(), url: "3", score: 30) - try await [p1, p2, p3].save(on: app.db) - try await Repository(package: p1, - defaultBranch: "main", - name: "1", - owner: "foo", - summary: "").save(on: app.db) - try await Repository(package: p2, - defaultBranch: "main", - name: "2", - owner: "foo", - summary: "").save(on: app.db) - try await Repository(package: p3, - defaultBranch: "main", - name: "3", - owner: "foo", - summary: "foo bar").save(on: app.db) - try await Version(package: p1, packageName: "Foo bar", reference: .branch("main")) - .save(on: app.db) - try await Version(package: p2, packageName: "foobar", reference: .branch("main")) - .save(on: app.db) - try await Version(package: p3, packageName: "some name", reference: .branch("main")) - .save(on: app.db) - try await Search.refresh(on: app.db) + try await withApp { app in + // setup + // We have three packages that all match in some way: + // 1: exact package name match - we want this one to be at the top + // 2: package name contains search term + // 3: summary contains search term + let p1 = Package(id: UUID(), url: "1", score: 10) + let p2 = Package(id: UUID(), url: "2", score: 20) + let p3 = Package(id: UUID(), url: "3", score: 30) + try await [p1, p2, p3].save(on: app.db) + try await Repository(package: p1, + defaultBranch: "main", + name: "1", + owner: "foo", + summary: "").save(on: app.db) + try await Repository(package: p2, + defaultBranch: "main", + name: "2", + owner: "foo", + summary: "").save(on: app.db) + try await Repository(package: p3, + defaultBranch: "main", + name: "3", + owner: "foo", + summary: "foo bar").save(on: app.db) + try await Version(package: p1, packageName: "Foo bar", reference: .branch("main")) + .save(on: app.db) + try await Version(package: p2, packageName: "foobar", reference: .branch("main")) + .save(on: app.db) + try await Version(package: p3, packageName: "some name", reference: .branch("main")) + .save(on: app.db) + try await Search.refresh(on: app.db) - // MUT - let res = try await Search.fetch(app.db, ["foo", "bar"], page: 1, pageSize: 20) + // MUT + let res = try await Search.fetch(app.db, ["foo", "bar"], page: 1, pageSize: 20) - XCTAssertEqual(res.results.map(\.package?.repositoryName), - ["1", "3", "2"]) + #expect(res.results.map(\.package?.repositoryName) == ["1", "3", "2"]) + } } - func test_exact_name_null_packageName() async throws { + @Test func exact_name_null_packageName() async throws { // Ensure null packageName value aren't boosted // https://github.com/SwiftPackageIndex/SwiftPackageIndex-Server/issues/2072 - // setup - // We have three packages that match the search term "bar" via their summary. - // The third package has no package name. This test ensure it's not boosted - // to the front. - let p1 = Package(id: UUID(), url: "1", score: 30) - let p2 = Package(id: UUID(), url: "2", score: 20) - let p3 = Package(id: UUID(), url: "3", score: 10) - try await [p1, p2, p3].save(on: app.db) - try await Repository(package: p1, - defaultBranch: "main", - name: "1", - owner: "foo", - summary: "bar1").save(on: app.db) - try await Repository(package: p2, - defaultBranch: "main", - name: "2", - owner: "foo", - summary: "bar2").save(on: app.db) - try await Repository(package: p3, - defaultBranch: "main", - name: "3", - owner: "foo", - summary: "bar3").save(on: app.db) - try await Version(package: p1, packageName: "Bar1", reference: .branch("main")) - .save(on: app.db) - try await Version(package: p2, packageName: "Bar2", reference: .branch("main")) - .save(on: app.db) - try await Version(package: p3, packageName: nil, reference: .branch("main")) - .save(on: app.db) - try await Search.refresh(on: app.db) + try await withApp { app in + // setup + // We have three packages that match the search term "bar" via their summary. + // The third package has no package name. This test ensure it's not boosted + // to the front. + let p1 = Package(id: UUID(), url: "1", score: 30) + let p2 = Package(id: UUID(), url: "2", score: 20) + let p3 = Package(id: UUID(), url: "3", score: 10) + try await [p1, p2, p3].save(on: app.db) + try await Repository(package: p1, + defaultBranch: "main", + name: "1", + owner: "foo", + summary: "bar1").save(on: app.db) + try await Repository(package: p2, + defaultBranch: "main", + name: "2", + owner: "foo", + summary: "bar2").save(on: app.db) + try await Repository(package: p3, + defaultBranch: "main", + name: "3", + owner: "foo", + summary: "bar3").save(on: app.db) + try await Version(package: p1, packageName: "Bar1", reference: .branch("main")) + .save(on: app.db) + try await Version(package: p2, packageName: "Bar2", reference: .branch("main")) + .save(on: app.db) + try await Version(package: p3, packageName: nil, reference: .branch("main")) + .save(on: app.db) + try await Search.refresh(on: app.db) - // MUT - let res = try await Search.fetch(app.db, ["bar"], page: 1, pageSize: 20) + // MUT + let res = try await Search.fetch(app.db, ["bar"], page: 1, pageSize: 20) - XCTAssertEqual(res.results.map(\.package?.repositoryName), - ["1", "2", "3"]) + #expect(res.results.map(\.package?.repositoryName) == ["1", "2", "3"]) + } } - func test_exclude_null_fields() async throws { + @Test func exclude_null_fields() async throws { // Ensure excluding results with NULL fields - // setup: - // Packages that all match but each having one NULL for a required field - let p1 = Package(id: UUID(), url: "1", score: 10) - let p2 = Package(id: UUID(), url: "2", score: 20) - try await [p1, p2].save(on: app.db) - try await Repository(package: p1, - defaultBranch: "main", - name: nil, // Missing repository name - owner: "foobar", - summary: "").save(on: app.db) - try await Repository(package: p2, - defaultBranch: "main", - name: "2", - owner: nil, // Missing repository owner - summary: "foo bar").save(on: app.db) - try await Version(package: p1, packageName: "foo1", reference: .branch("main")) - .save(on: app.db) - try await Version(package: p2, packageName: "foo2", reference: .branch("main")) - .save(on: app.db) - try await Search.refresh(on: app.db) + try await withApp { app in + // setup: + // Packages that all match but each having one NULL for a required field + let p1 = Package(id: UUID(), url: "1", score: 10) + let p2 = Package(id: UUID(), url: "2", score: 20) + try await [p1, p2].save(on: app.db) + try await Repository(package: p1, + defaultBranch: "main", + name: nil, // Missing repository name + owner: "foobar", + summary: "").save(on: app.db) + try await Repository(package: p2, + defaultBranch: "main", + name: "2", + owner: nil, // Missing repository owner + summary: "foo bar").save(on: app.db) + try await Version(package: p1, packageName: "foo1", reference: .branch("main")) + .save(on: app.db) + try await Version(package: p2, packageName: "foo2", reference: .branch("main")) + .save(on: app.db) + try await Search.refresh(on: app.db) - // MUT - let res = try await Search.fetch(app.db, ["foo"], page: 1, pageSize: 20) + // MUT + let res = try await Search.fetch(app.db, ["foo"], page: 1, pageSize: 20) - // ensure only the author result is coming through, not the packages - XCTAssertEqual(res.results.map(\.testDescription), ["a:foobar"]) + // ensure only the author result is coming through, not the packages + #expect(res.results.map(\.testDescription) == ["a:foobar"]) + } } - func test_include_null_package_name() async throws { + @Test func include_null_package_name() async throws { // Ensure that packages that somehow have a NULL package name do *not* get excluded from search results. - let p1 = Package(id: .id0, url: "1", score: 10) - try await p1.save(on: app.db) + try await withApp { app in + let p1 = Package(id: .id0, url: "1", score: 10) + try await p1.save(on: app.db) - try await Repository(package: p1, - defaultBranch: "main", - name: "1", - owner: "bar", - summary: "foo and bar").save(on: app.db) + try await Repository(package: p1, + defaultBranch: "main", + name: "1", + owner: "bar", + summary: "foo and bar").save(on: app.db) - // Version record with a missing package name. - try await Version(package: p1, packageName: nil, reference: .branch("main")) - .save(on: app.db) + // Version record with a missing package name. + try await Version(package: p1, packageName: nil, reference: .branch("main")) + .save(on: app.db) - try await Search.refresh(on: app.db) + try await Search.refresh(on: app.db) - // MUT - let res = try await Search.fetch(app.db, ["foo"], page: 1, pageSize: 20) + // MUT + let res = try await Search.fetch(app.db, ["foo"], page: 1, pageSize: 20) - let packageResult = try XCTUnwrap(res.results.first?.package) - XCTAssertEqual(packageResult.packageId, .id0) - XCTAssertEqual(packageResult.repositoryName, "1") - XCTAssertEqual(packageResult.repositoryOwner, "bar") - XCTAssertEqual(packageResult.packageName, nil) + let packageResult = try #require(res.results.first?.package) + #expect(packageResult.packageId == .id0) + #expect(packageResult.repositoryName == "1") + #expect(packageResult.repositoryOwner == "bar") + #expect(packageResult.packageName == nil) + } } - func test_exact_word_match() async throws { + @Test func exact_word_match() async throws { // Ensure exact word matches are boosted // See also https://github.com/SwiftPackageIndex/SwiftPackageIndex-Server/issues/2072 - // setup - // We have three packages that all match the search term "ping". This test - // ensures the one with the whole word match is boosted to the front - // despite having the lowest score. - let p1 = Package(id: UUID(), url: "1", score: 30) - let p2 = Package(id: UUID(), url: "2", score: 20) - let p3 = Package(id: UUID(), url: "3", score: 10) - try await [p1, p2, p3].save(on: app.db) - try await Repository(package: p1, - defaultBranch: "main", - name: "1", - owner: "foo", - summary: "mapping").save(on: app.db) - try await Repository(package: p2, - defaultBranch: "main", - name: "2", - owner: "foo", - summary: "flopping").save(on: app.db) - try await Repository(package: p3, - defaultBranch: "main", - name: "3", - owner: "foo", - summary: "ping").save(on: app.db) - try await Version(package: p1, packageName: "Foo1", reference: .branch("main")) - .save(on: app.db) - try await Version(package: p2, packageName: "Foo2", reference: .branch("main")) - .save(on: app.db) - try await Version(package: p3, packageName: "Foo3", reference: .branch("main")) - .save(on: app.db) - try await Search.refresh(on: app.db) + try await withApp { app in + // setup + // We have three packages that all match the search term "ping". This test + // ensures the one with the whole word match is boosted to the front + // despite having the lowest score. + let p1 = Package(id: UUID(), url: "1", score: 30) + let p2 = Package(id: UUID(), url: "2", score: 20) + let p3 = Package(id: UUID(), url: "3", score: 10) + try await [p1, p2, p3].save(on: app.db) + try await Repository(package: p1, + defaultBranch: "main", + name: "1", + owner: "foo", + summary: "mapping").save(on: app.db) + try await Repository(package: p2, + defaultBranch: "main", + name: "2", + owner: "foo", + summary: "flopping").save(on: app.db) + try await Repository(package: p3, + defaultBranch: "main", + name: "3", + owner: "foo", + summary: "ping").save(on: app.db) + try await Version(package: p1, packageName: "Foo1", reference: .branch("main")) + .save(on: app.db) + try await Version(package: p2, packageName: "Foo2", reference: .branch("main")) + .save(on: app.db) + try await Version(package: p3, packageName: "Foo3", reference: .branch("main")) + .save(on: app.db) + try await Search.refresh(on: app.db) - // MUT - let res = try await Search.fetch(app.db, ["ping"], page: 1, pageSize: 20) + // MUT + let res = try await Search.fetch(app.db, ["ping"], page: 1, pageSize: 20) - // validate - XCTAssertEqual(res.results.map(\.package?.repositoryName), - ["3", "1", "2"]) + // validate + #expect(res.results.map(\.package?.repositoryName) == ["3", "1", "2"]) + } } - func test_repo_word_match() async throws { + @Test func repo_word_match() async throws { // Ensure the repository name is part of word matching // See also https://github.com/SwiftPackageIndex/SwiftPackageIndex-Server/issues/2263 // We have two packages that both match the search term "syntax". This test // ensures the one where the match is only in the repository name gets still // ranked first due to its higher score. - let p1 = Package(id: UUID(), url: "foo/bar", score: 10) - let p2 = Package(id: UUID(), url: "foo/swift-syntax", score: 20) - try await [p1, p2].save(on: app.db) - try await Repository(package: p1, - defaultBranch: "main", - name: "bar", - owner: "foo", - summary: "syntax").save(on: app.db) - try await Repository(package: p2, - defaultBranch: "main", - name: "swift-syntax", - owner: "foo").save(on: app.db) - try await Version(package: p1, packageName: "Bar", reference: .branch("main")) - .save(on: app.db) - try await Version(package: p2, packageName: "SwiftSyntax", reference: .branch("main")) - .save(on: app.db) - try await Search.refresh(on: app.db) + try await withApp { app in + let p1 = Package(id: UUID(), url: "foo/bar", score: 10) + let p2 = Package(id: UUID(), url: "foo/swift-syntax", score: 20) + try await [p1, p2].save(on: app.db) + try await Repository(package: p1, + defaultBranch: "main", + name: "bar", + owner: "foo", + summary: "syntax").save(on: app.db) + try await Repository(package: p2, + defaultBranch: "main", + name: "swift-syntax", + owner: "foo").save(on: app.db) + try await Version(package: p1, packageName: "Bar", reference: .branch("main")) + .save(on: app.db) + try await Version(package: p2, packageName: "SwiftSyntax", reference: .branch("main")) + .save(on: app.db) + try await Search.refresh(on: app.db) - // MUT - let res = try await Search.fetch(app.db, ["syntax"], page: 1, pageSize: 20) + // MUT + let res = try await Search.fetch(app.db, ["syntax"], page: 1, pageSize: 20) - // validate - XCTAssertEqual(res.results.map(\.package?.repositoryName), - ["swift-syntax", "bar"]) + // validate + #expect(res.results.map(\.package?.repositoryName) == ["swift-syntax", "bar"]) + } } - func test_sanitize() throws { - XCTAssertEqual(Search.sanitize(["*"]), ["\\*"]) - XCTAssertEqual(Search.sanitize(["?"]), ["\\?"]) - XCTAssertEqual(Search.sanitize(["("]), ["\\("]) - XCTAssertEqual(Search.sanitize([")"]), ["\\)"]) - XCTAssertEqual(Search.sanitize(["["]), ["\\["]) - XCTAssertEqual(Search.sanitize(["]"]), ["\\]"]) - XCTAssertEqual(Search.sanitize(["\\"]), []) - XCTAssertEqual(Search.sanitize(["test\\"]), ["test"]) + @Test func sanitize() async throws { + #expect(Search.sanitize(["*"]) == ["\\*"]) + #expect(Search.sanitize(["?"]) == ["\\?"]) + #expect(Search.sanitize(["("]) == ["\\("]) + #expect(Search.sanitize([")"]) == ["\\)"]) + #expect(Search.sanitize(["["]) == ["\\["]) + #expect(Search.sanitize(["]"]) == ["\\]"]) + #expect(Search.sanitize(["\\"]) == []) + #expect(Search.sanitize(["test\\"]) == ["test"]) } - func test_invalid_characters() async throws { + @Test func invalid_characters() async throws { // https://github.com/SwiftPackageIndex/SwiftPackageIndex-Server/issues/974 // https://github.com/SwiftPackageIndex/SwiftPackageIndex-Server/issues/1402 // Ensure we don't raise a 500 for certain characters // "server: invalid regular expression: quantifier operand invalid" - - do { - // MUT - let res = try await Search.fetch(app.db, ["*"], page: 1, pageSize: 20) - - // validation - XCTAssertEqual(res, .init(hasMoreResults: false, searchTerm: "\\*", searchFilters: [], results: [])) - } - - do { - // MUT - let res = try await Search.fetch(app.db, ["\\"], page: 1, pageSize: 20) - - // validation - XCTAssertEqual(res, .init(hasMoreResults: false, searchTerm: "", searchFilters: [], results: [])) + try await withApp { app in + do { + // MUT + let res = try await Search.fetch(app.db, ["*"], page: 1, pageSize: 20) + + // validation + #expect(res == .init(hasMoreResults: false, searchTerm: "\\*", searchFilters: [], results: [])) + } + + do { + // MUT + let res = try await Search.fetch(app.db, ["\\"], page: 1, pageSize: 20) + + // validation + #expect(res == .init(hasMoreResults: false, searchTerm: "", searchFilters: [], results: [])) + } } } - func test_search_keyword() async throws { + @Test func search_keyword() async throws { // Test searching for a keyword // setup // p1: decoy // p2: match - let pkgs = (0..<2).map { Package(id: UUID(), url: "\($0)".url) } - try await pkgs.save(on: app.db) - try await [ - Repository(package: pkgs[0], - defaultBranch: "main", - name: "0", - owner: "foo"), - Repository(package: pkgs[1], - defaultBranch: "main", - keywords: ["topic"], - name: "1", - owner: "foo") - ].save(on: app.db) - try await [ - Version(package: pkgs[0], packageName: "p0", reference: .branch("main")), - Version(package: pkgs[1], packageName: "p1", reference: .branch("main")) - ].save(on: app.db) - try await Search.refresh(on: app.db) - - // MUT - let res = try await Search.fetch(app.db, ["topic"], page: 1, pageSize: 20) - - XCTAssertEqual(res.results.map(\.testDescription), ["k:topic", "p:p1"]) + try await withApp { app in + let pkgs = (0..<2).map { Package(id: UUID(), url: "\($0)".url) } + try await pkgs.save(on: app.db) + try await [ + Repository(package: pkgs[0], + defaultBranch: "main", + name: "0", + owner: "foo"), + Repository(package: pkgs[1], + defaultBranch: "main", + keywords: ["topic"], + name: "1", + owner: "foo") + ].save(on: app.db) + try await [ + Version(package: pkgs[0], packageName: "p0", reference: .branch("main")), + Version(package: pkgs[1], packageName: "p1", reference: .branch("main")) + ].save(on: app.db) + try await Search.refresh(on: app.db) + + // MUT + let res = try await Search.fetch(app.db, ["topic"], page: 1, pageSize: 20) + + #expect(res.results.map(\.testDescription) == ["k:topic", "p:p1"]) + } } - func test_search_keyword_multiple_results() async throws { + @Test func search_keyword_multiple_results() async throws { // Test searching with multiple keyword results // setup // p1: decoy // p2: match - let pkgs = (0..<4).map { Package(id: UUID(), url: "\($0)".url, score: $0) } - try await pkgs.save(on: app.db) - let keywords = [ - [], - ["topic"], - ["atopicb"], - ["topicb"], - ] - try await (0..<4).map { - try Repository(package: pkgs[$0], - defaultBranch: "main", - keywords: keywords[$0], - name: "\($0)", - owner: "foo") - }.save(on: app.db) - try await (0..<4).map { - try Version(package: pkgs[$0], packageName: "p\($0)", reference: .branch("main")) - }.save(on: app.db) - try await Search.refresh(on: app.db) - - // MUT - let res = try await Search.fetch(app.db, ["topic"], page: 1, pageSize: 20) - - // validate - // The keyword results are unordered in SQL because they're ordered by frequency - // after fetching. We sort them here for stable test results. - // (packages are also matched via their keywords) - XCTAssertEqual(res.results.map(\.testDescription).sorted(), - ["k:atopicb", "k:topic", "k:topicb", "p:p1", "p:p2", "p:p3"]) + try await withApp { app in + let pkgs = (0..<4).map { Package(id: UUID(), url: "\($0)".url, score: $0) } + try await pkgs.save(on: app.db) + let keywords = [ + [], + ["topic"], + ["atopicb"], + ["topicb"], + ] + try await (0..<4).map { + try Repository(package: pkgs[$0], + defaultBranch: "main", + keywords: keywords[$0], + name: "\($0)", + owner: "foo") + }.save(on: app.db) + try await (0..<4).map { + try Version(package: pkgs[$0], packageName: "p\($0)", reference: .branch("main")) + }.save(on: app.db) + try await Search.refresh(on: app.db) + + // MUT + let res = try await Search.fetch(app.db, ["topic"], page: 1, pageSize: 20) + + // validate + // The keyword results are unordered in SQL because they're ordered by frequency + // after fetching. We sort them here for stable test results. + // (packages are also matched via their keywords) + #expect(res.results.map(\.testDescription).sorted() == ["k:atopicb", "k:topic", "k:topicb", "p:p1", "p:p2", "p:p3"]) + } } - func test_search_author_multiple_results() async throws { + @Test func search_author_multiple_results() async throws { // Test searching with multiple authors results // setup // p1: decoy // p2: match - let pkgs = (0..<4).map { - Package(id: UUID(), url: "\($0)".url, score: $0) + try await withApp { app in + let pkgs = (0..<4).map { + Package(id: UUID(), url: "\($0)".url, score: $0) + } + try await pkgs.save(on: app.db) + let authors = [ + "some-other", + "another-author", + "author", + "author-2", + ] + try await (0..<4).map { + try Repository(package: pkgs[$0], + defaultBranch: "main", + name: "\($0)", + owner: authors[$0]) + }.save(on: app.db) + try await (0..<4).map { + try Version(package: pkgs[$0], packageName: "p\($0)", reference: .branch("main")) + }.save(on: app.db) + try await Search.refresh(on: app.db) + + // MUT + let res = try await Search.fetch(app.db, ["author"], page: 1, pageSize: 20) + + // validate that keyword results are ordered by levenshtein distance + // (packages are also matched via their keywords) + #expect(res.results.map(\.testDescription) == ["a:author", "a:author-2", "a:another-author", "p:p3", "p:p2", "p:p1"]) } - try await pkgs.save(on: app.db) - let authors = [ - "some-other", - "another-author", - "author", - "author-2", - ] - try await (0..<4).map { - try Repository(package: pkgs[$0], - defaultBranch: "main", - name: "\($0)", - owner: authors[$0]) - }.save(on: app.db) - try await (0..<4).map { - try Version(package: pkgs[$0], packageName: "p\($0)", reference: .branch("main")) - }.save(on: app.db) - try await Search.refresh(on: app.db) - - // MUT - let res = try await Search.fetch(app.db, ["author"], page: 1, pageSize: 20) - - // validate that keyword results are ordered by levenshtein distance - // (packages are also matched via their keywords) - XCTAssertEqual(res.results.map(\.testDescription), - ["a:author", "a:author-2", "a:another-author", "p:p3", "p:p2", "p:p1"]) } - func test_search_author() async throws { + @Test func search_author() async throws { // Test searching for an author // setup // p1: decoy // p2: match - let p1 = Package(id: .id1, url: "1", score: 10) - let p2 = Package(id: .id2, url: "2", score: 20) - try await [p1, p2].save(on: app.db) - try await Repository(package: p1, - defaultBranch: "main", - name: "1", - owner: "bar", - summary: "").save(on: app.db) - try await Repository(package: p2, - defaultBranch: "main", - lastCommitDate: .t0, - name: "2", - owner: "foo", - stars: 1234, - summary: "").save(on: app.db) - try await Version(package: p1, packageName: "p1", reference: .branch("main")) - .save(on: app.db) - try await Version(package: p2, packageName: "p2", reference: .branch("main")) - .save(on: app.db) - try await Search.refresh(on: app.db) - - // MUT - let res = try await Search.fetch(app.db, ["foo"], page: 1, pageSize: 20) - - XCTAssertEqual(res.results, [ - .author(.init(name: "foo")), - // the owner fields is part of the package match, so we always also match packages by an author when searching for an author - .package(.init(packageId: .id2, - packageName: "p2", - packageURL: "/foo/2", - repositoryName: "2", - repositoryOwner: "foo", - stars: 1234, - lastActivityAt: .t0, - summary: "", - keywords: [], - hasDocs: false)!) - ]) + try await withApp { app in + let p1 = Package(id: .id1, url: "1", score: 10) + let p2 = Package(id: .id2, url: "2", score: 20) + try await [p1, p2].save(on: app.db) + try await Repository(package: p1, + defaultBranch: "main", + name: "1", + owner: "bar", + summary: "").save(on: app.db) + try await Repository(package: p2, + defaultBranch: "main", + lastCommitDate: .t0, + name: "2", + owner: "foo", + stars: 1234, + summary: "").save(on: app.db) + try await Version(package: p1, packageName: "p1", reference: .branch("main")) + .save(on: app.db) + try await Version(package: p2, packageName: "p2", reference: .branch("main")) + .save(on: app.db) + try await Search.refresh(on: app.db) + + // MUT + let res = try await Search.fetch(app.db, ["foo"], page: 1, pageSize: 20) + + #expect(res.results == [ + .author(.init(name: "foo")), + // the owner fields is part of the package match, so we always also match packages by an author when searching for an author + .package(.init(packageId: .id2, + packageName: "p2", + packageURL: "/foo/2", + repositoryName: "2", + repositoryOwner: "foo", + stars: 1234, + lastActivityAt: .t0, + summary: "", + keywords: [], + hasDocs: false)!) + ]) + } } - func test_search_module_name() async throws { + @Test func search_module_name() async throws { // Test searching for a term that only appears in a module (target) name // setup // p1: decoy // p2: match - let p1 = Package(id: .id1, url: "1", score: 10) - let p2 = Package(id: .id2, url: "2", score: 20) - try await [p1, p2].save(on: app.db) - try await Repository(package: p1, defaultBranch: "main", name: "1", owner: "foo").save(on: app.db) - try await Repository(package: p2, defaultBranch: "main", name: "2", owner: "foo").save(on: app.db) - try await Version(package: p1, packageName: "p1", reference: .branch("main")) - .save(on: app.db) - let v2 = try Version(package: p2, latest: .defaultBranch, packageName: "p2", reference: .branch("main")) - try await v2.save(on: app.db) - try await Product(version: v2, type: .library(.automatic), name: "ModuleName").save(on: app.db) - try await Search.refresh(on: app.db) - - // MUT - let res = try await Search.fetch(app.db, ["modulename"], page: 1, pageSize: 20) - - XCTAssertEqual(res.results.count, 1) - XCTAssertEqual(res.results, [ - .package(.init(packageId: .id2, - packageName: "p2", - packageURL: "/foo/2", - repositoryName: "2", - repositoryOwner: "foo", - stars: 0, - lastActivityAt: nil, - summary: nil, - keywords: [], - hasDocs: false)!) - ]) - } - - func test_search_withoutTerms() async throws { - // Setup - let p1 = Package(id: .id1, url: "1", score: 10) - let p2 = Package(id: .id2, url: "2", score: 20) - try await [p1, p2].save(on: app.db) - try await Repository(package: p1, - defaultBranch: "main", - keywords: ["a"], - name: "1", - owner: "bar", - stars: 50, - summary: "test package").save(on: app.db) - try await Repository(package: p2, - defaultBranch: "main", - keywords: ["b"], - name: "2", - owner: "foo", - stars: 10, - summary: "test package").save(on: app.db) - try await Version(package: p1, packageName: "p1", reference: .branch("main")) - .save(on: app.db) - try await Version(package: p2, packageName: "p2", reference: .branch("main")) - .save(on: app.db) - try await Search.refresh(on: app.db) - - // MUT - let res = try await Search.fetch(app.db, ["stars:>15"], page: 1, pageSize: 20) - XCTAssertEqual(res.results.count, 1) - XCTAssertEqual(res.results.compactMap(\.package).compactMap(\.packageName).sorted(), ["p1"]) - } - - func test_search_withFilter_stars() async throws { - // Setup - let p1 = Package(id: .id1, url: "1", score: 10) - let p2 = Package(id: .id2, url: "2", score: 20) - try await [p1, p2].save(on: app.db) - try await Repository(package: p1, - defaultBranch: "main", - name: "1", - owner: "bar", - stars: 50, - summary: "test package").save(on: app.db) - try await Repository(package: p2, - defaultBranch: "main", - name: "2", - owner: "foo", - stars: 10, - summary: "test package").save(on: app.db) - try await Version(package: p1, packageName: "p1", reference: .branch("main")) - .save(on: app.db) - try await Version(package: p2, packageName: "p2", reference: .branch("main")) - .save(on: app.db) - try await Search.refresh(on: app.db) - - do { // Baseline - let res = try await Search.fetch(app.db, ["test"], page: 1, pageSize: 20) - XCTAssertEqual(res.results.count, 2) - XCTAssertEqual(res.results.compactMap(\.package).compactMap(\.packageName).sorted(), ["p1", "p2"]) - } - - do { // Greater Than - let res = try await Search.fetch(app.db, ["test", "stars:>25"], page: 1, pageSize: 20) - XCTAssertEqual(res.results.count, 1) - XCTAssertEqual(res.results.first?.package?.packageName, "p1") - } - - do { // Less Than - let res = try await Search.fetch(app.db, ["test", "stars:<25"], page: 1, pageSize: 20) - XCTAssertEqual(res.results.count, 1) - XCTAssertEqual(res.results.first?.package?.packageName, "p2") - } - - do { // Equal - let res = try await Search.fetch(app.db, ["test", "stars:50"], page: 1, pageSize: 20) - XCTAssertEqual(res.results.count, 1) - XCTAssertEqual(res.results.first?.package?.packageName, "p1") - } - - do { // Not Equals - let res = try await Search.fetch(app.db, ["test", "stars:!50"], page: 1, pageSize: 20) - XCTAssertEqual(res.results.count, 1) - XCTAssertEqual(res.results.first?.package?.packageName, "p2") - } - } - - func test_onlyPackageResults_whenFiltersApplied() throws { - do { // with filter - let query = try XCTUnwrap(Search.query(app.db, ["a", "stars:500"], page: 1, pageSize: 5)) - let sql = renderSQL(query) - XCTAssertTrue(sql.contains(#"SELECT DISTINCT 'author' AS "match_type""#)) - XCTAssertTrue(sql.contains(#"SELECT DISTINCT 'keyword' AS "match_type""#)) - XCTAssertTrue(sql.contains(#"SELECT 'package' AS "match_type""#)) - } - - do { // without filter - let query = try XCTUnwrap(Search.query(app.db, ["a"], page: 1, pageSize: 5)) - let sql = renderSQL(query) - XCTAssertTrue(sql.contains(#"SELECT DISTINCT 'author' AS "match_type""#)) - XCTAssertTrue(sql.contains(#"SELECT DISTINCT 'keyword' AS "match_type""#)) - XCTAssertTrue(sql.contains(#"SELECT 'package' AS "match_type""#)) - } - } + try await withApp { app in + let p1 = Package(id: .id1, url: "1", score: 10) + let p2 = Package(id: .id2, url: "2", score: 20) + try await [p1, p2].save(on: app.db) + try await Repository(package: p1, defaultBranch: "main", name: "1", owner: "foo").save(on: app.db) + try await Repository(package: p2, defaultBranch: "main", name: "2", owner: "foo").save(on: app.db) + try await Version(package: p1, packageName: "p1", reference: .branch("main")) + .save(on: app.db) + let v2 = try Version(package: p2, latest: .defaultBranch, packageName: "p2", reference: .branch("main")) + try await v2.save(on: app.db) + try await Product(version: v2, type: .library(.automatic), name: "ModuleName").save(on: app.db) + try await Search.refresh(on: app.db) - func test_authorSearchFilter() async throws { - // Setup - let p1 = Package(url: "1", platformCompatibility: [.iOS]) - let p2 = Package(url: "2", platformCompatibility: [.macOS]) - try await [p1, p2].save(on: app.db) - try await Repository(package: p1, - defaultBranch: "main", - name: "1", - owner: "foo", - summary: "test package").save(on: app.db) - try await Repository(package: p2, - defaultBranch: "main", - name: "2", - owner: "bar", - summary: "test package").save(on: app.db) - try await Version(package: p1, packageName: "p1", reference: .branch("main")) - .save(on: app.db) - try await Version(package: p2, packageName: "p2", reference: .branch("main")) - .save(on: app.db) - try await Search.refresh(on: app.db) - - do { // MUT - let res = try await Search.fetch(app.db, ["test", "author:foo"], page: 1, pageSize: 20) - - // validate - XCTAssertEqual(res.results.compactMap(\.packageResult?.repositoryName), ["1"]) - } - - do { // double check that leaving the filter term off selects both packages - // MUT - let res = try await Search.fetch(app.db, ["test"], page: 1, pageSize: 20) - - // validate - XCTAssertEqual( - res.results.compactMap(\.packageResult?.repositoryName).sorted(), - ["1", "2"] - ) + let res = try await Search.fetch(app.db, ["modulename"], page: 1, pageSize: 20) + + #expect(res.results.count == 1) + #expect(res.results == [ + .package(.init(packageId: .id2, + packageName: "p2", + packageURL: "/foo/2", + repositoryName: "2", + repositoryOwner: "foo", + stars: 0, + lastActivityAt: nil, + summary: nil, + keywords: [], + hasDocs: false)!) + ]) } } - func test_keywordSearchFilter() async throws { - // Setup - let p1 = Package(url: "1", platformCompatibility: [.iOS]) - let p2 = Package(url: "2", platformCompatibility: [.macOS]) - try await [p1, p2].save(on: app.db) - try await Repository(package: p1, - defaultBranch: "main", - keywords: ["kw1", "kw2"], - name: "1", - owner: "foo", - summary: "test package").save(on: app.db) - try await Repository(package: p2, - defaultBranch: "main", - keywords: ["kw1-2"], - name: "2", - owner: "bar", - summary: "test package").save(on: app.db) - try await Version(package: p1, packageName: "p1", reference: .branch("main")) - .save(on: app.db) - try await Version(package: p2, packageName: "p2", reference: .branch("main")) - .save(on: app.db) - try await Search.refresh(on: app.db) + @Test func search_withoutTerms() async throws { + try await withApp { app in + // Setup + let p1 = Package(id: .id1, url: "1", score: 10) + let p2 = Package(id: .id2, url: "2", score: 20) + try await [p1, p2].save(on: app.db) + try await Repository(package: p1, + defaultBranch: "main", + keywords: ["a"], + name: "1", + owner: "bar", + stars: 50, + summary: "test package").save(on: app.db) + try await Repository(package: p2, + defaultBranch: "main", + keywords: ["b"], + name: "2", + owner: "foo", + stars: 10, + summary: "test package").save(on: app.db) + try await Version(package: p1, packageName: "p1", reference: .branch("main")) + .save(on: app.db) + try await Version(package: p2, packageName: "p2", reference: .branch("main")) + .save(on: app.db) + try await Search.refresh(on: app.db) - do { // MUT - let res = try await Search.fetch(app.db, ["test", "keyword:kw1"], page: 1, pageSize: 20) - - // validate - XCTAssertEqual(res.results.compactMap(\.packageResult?.repositoryName), ["1"]) - } - - do { // double check that leaving the filter term off selects both packages - // MUT - let res = try await Search.fetch(app.db, ["test"], page: 1, pageSize: 20) - - // validate - XCTAssertEqual( - res.results.compactMap(\.packageResult?.repositoryName).sorted(), - ["1", "2"] - ) + let res = try await Search.fetch(app.db, ["stars:>15"], page: 1, pageSize: 20) + #expect(res.results.count == 1) + #expect(res.results.compactMap(\.package).compactMap(\.packageName).sorted() == ["p1"]) } } - func test_lastActivitySearchFilter() async throws { - // Setup - let p1 = Package(url: "1", platformCompatibility: [.iOS]) - let p2 = Package(url: "2", platformCompatibility: [.macOS]) - try await [p1, p2].save(on: app.db) - try await Repository(package: p1, - defaultBranch: "main", - lastCommitDate: .t0.adding(days: -1), - name: "1", - owner: "foo", - summary: "test package").save(on: app.db) - try await Repository(package: p2, - defaultBranch: "main", - lastCommitDate: .t0, - name: "2", - owner: "bar", - summary: "test package").save(on: app.db) - try await Version(package: p1, packageName: "p1", reference: .branch("main")) - .save(on: app.db) - try await Version(package: p2, packageName: "p2", reference: .branch("main")) - .save(on: app.db) - try await Search.refresh(on: app.db) - - do { - // MUT - let res = try await Search.fetch(app.db, ["test", "last_activity:<1970-01-01"], page: 1, pageSize: 20) - - // validate - XCTAssertEqual(res.results.compactMap(\.packageResult?.repositoryName), ["1"]) - } - - do { // double check that leaving the filter term off selects both packages - // MUT - let res = try await Search.fetch(app.db, ["test"], page: 1, pageSize: 20) - - // validate - XCTAssertEqual( - res.results.compactMap(\.packageResult?.repositoryName).sorted(), - ["1", "2"] - ) + @Test func search_withFilter_stars() async throws { + try await withApp { app in + // Setup + let p1 = Package(id: .id1, url: "1", score: 10) + let p2 = Package(id: .id2, url: "2", score: 20) + try await [p1, p2].save(on: app.db) + try await Repository(package: p1, + defaultBranch: "main", + name: "1", + owner: "bar", + stars: 50, + summary: "test package").save(on: app.db) + try await Repository(package: p2, + defaultBranch: "main", + name: "2", + owner: "foo", + stars: 10, + summary: "test package").save(on: app.db) + try await Version(package: p1, packageName: "p1", reference: .branch("main")) + .save(on: app.db) + try await Version(package: p2, packageName: "p2", reference: .branch("main")) + .save(on: app.db) + try await Search.refresh(on: app.db) + + do { // Baseline + let res = try await Search.fetch(app.db, ["test"], page: 1, pageSize: 20) + #expect(res.results.count == 2) + #expect(res.results.compactMap(\.package).compactMap(\.packageName).sorted() == ["p1", "p2"]) + } + + do { // Greater Than + let res = try await Search.fetch(app.db, ["test", "stars:>25"], page: 1, pageSize: 20) + #expect(res.results.count == 1) + #expect(res.results.first?.package?.packageName == "p1") + } + + do { // Less Than + let res = try await Search.fetch(app.db, ["test", "stars:<25"], page: 1, pageSize: 20) + #expect(res.results.count == 1) + #expect(res.results.first?.package?.packageName == "p2") + } + + do { // Equal + let res = try await Search.fetch(app.db, ["test", "stars:50"], page: 1, pageSize: 20) + #expect(res.results.count == 1) + #expect(res.results.first?.package?.packageName == "p1") + } + + do { // Not Equals + let res = try await Search.fetch(app.db, ["test", "stars:!50"], page: 1, pageSize: 20) + #expect(res.results.count == 1) + #expect(res.results.first?.package?.packageName == "p2") + } } } - func test_lastCommitSearchFilter() async throws { - // Setup - let p1 = Package(url: "1", platformCompatibility: [.iOS]) - let p2 = Package(url: "2", platformCompatibility: [.macOS]) - try await [p1, p2].save(on: app.db) - try await Repository(package: p1, - defaultBranch: "main", - lastCommitDate: .t0.adding(days: -1), - name: "1", - owner: "foo", - summary: "test package").save(on: app.db) - try await Repository(package: p2, - defaultBranch: "main", - lastCommitDate: .t0, - name: "2", - owner: "bar", - summary: "test package").save(on: app.db) - try await Version(package: p1, packageName: "p1", reference: .branch("main")) - .save(on: app.db) - try await Version(package: p2, packageName: "p2", reference: .branch("main")) - .save(on: app.db) - try await Search.refresh(on: app.db) - - do { - // MUT - let res = try await Search.fetch(app.db, ["test", "last_commit:<1970-01-01"], page: 1, pageSize: 20) - - // validate - XCTAssertEqual(res.results.compactMap(\.packageResult?.repositoryName), ["1"]) - } - - do { // double check that leaving the filter term off selects both packages - // MUT - let res = try await Search.fetch(app.db, ["test"], page: 1, pageSize: 20) - - // validate - XCTAssertEqual( - res.results.compactMap(\.packageResult?.repositoryName).sorted(), - ["1", "2"] - ) + @Test func onlyPackageResults_whenFiltersApplied() async throws { + try await withApp { app in + do { // with filter + let query = try #require(Search.query(app.db, ["a", "stars:500"], page: 1, pageSize: 5)) + let sql = app.db.renderSQL(query) + #expect(sql.contains(#"SELECT DISTINCT 'author' AS "match_type""#)) + #expect(sql.contains(#"SELECT DISTINCT 'keyword' AS "match_type""#)) + #expect(sql.contains(#"SELECT 'package' AS "match_type""#)) + } + + do { // without filter + let query = try #require(Search.query(app.db, ["a"], page: 1, pageSize: 5)) + let sql = app.db.renderSQL(query) + #expect(sql.contains(#"SELECT DISTINCT 'author' AS "match_type""#)) + #expect(sql.contains(#"SELECT DISTINCT 'keyword' AS "match_type""#)) + #expect(sql.contains(#"SELECT 'package' AS "match_type""#)) + } } } - func test_licenseSearchFilter() async throws { - // Setup - let p1 = Package(url: "1", platformCompatibility: [.iOS]) - let p2 = Package(url: "2", platformCompatibility: [.macOS]) - try await [p1, p2].save(on: app.db) - try await Repository(package: p1, - defaultBranch: "main", - license: .mit, - name: "1", - owner: "foo", - summary: "test package").save(on: app.db) - try await Repository(package: p2, - defaultBranch: "main", - license: .none, - name: "2", - owner: "bar", - summary: "test package").save(on: app.db) - try await Version(package: p1, packageName: "p1", reference: .branch("main")) - .save(on: app.db) - try await Version(package: p2, packageName: "p2", reference: .branch("main")) - .save(on: app.db) - try await Search.refresh(on: app.db) - - do { - // MUT - let res = try await Search.fetch(app.db, ["test", "license:mit"], page: 1, pageSize: 20) - - // validate - XCTAssertEqual(res.results.compactMap(\.packageResult?.repositoryName), ["1"]) - } - - do { - // MUT - let res = try await Search.fetch(app.db, ["test", "license:compatible"], page: 1, pageSize: 20) - - // validate - XCTAssertEqual(res.results.compactMap(\.packageResult?.repositoryName), ["1"]) - } - - do { // double check that leaving the filter term off selects both packages - // MUT - let res = try await Search.fetch(app.db, ["test"], page: 1, pageSize: 20) - - // validate - XCTAssertEqual( - res.results.compactMap(\.packageResult?.repositoryName).sorted(), - ["1", "2"] - ) + @Test func authorSearchFilter() async throws { + try await withApp { app in + // Setup + let p1 = Package(url: "1", platformCompatibility: [.iOS]) + let p2 = Package(url: "2", platformCompatibility: [.macOS]) + try await [p1, p2].save(on: app.db) + try await Repository(package: p1, + defaultBranch: "main", + name: "1", + owner: "foo", + summary: "test package").save(on: app.db) + try await Repository(package: p2, + defaultBranch: "main", + name: "2", + owner: "bar", + summary: "test package").save(on: app.db) + try await Version(package: p1, packageName: "p1", reference: .branch("main")) + .save(on: app.db) + try await Version(package: p2, packageName: "p2", reference: .branch("main")) + .save(on: app.db) + try await Search.refresh(on: app.db) + + do { + // MUT + let res = try await Search.fetch(app.db, ["test", "author:foo"], page: 1, pageSize: 20) + + // validate + #expect(res.results.compactMap(\.packageResult?.repositoryName) == ["1"]) + } + + do { // double check that leaving the filter term off selects both packages + // MUT + let res = try await Search.fetch(app.db, ["test"], page: 1, pageSize: 20) + + // validate + #expect( + res.results.compactMap(\.packageResult?.repositoryName).sorted() == ["1", "2"] + ) + } } } - func test_platformSearchFilter() async throws { - // Setup - let p1 = Package(url: "1", platformCompatibility: [.iOS]) - let p2 = Package(url: "2", platformCompatibility: [.macOS]) - try await [p1, p2].save(on: app.db) - try await Repository(package: p1, - defaultBranch: "main", - name: "1", - owner: "foo", - summary: "test package").save(on: app.db) - try await Repository(package: p2, - defaultBranch: "main", - name: "2", - owner: "foo", - summary: "test package").save(on: app.db) - try await Version(package: p1, packageName: "p1", reference: .branch("main")) - .save(on: app.db) - try await Version(package: p2, packageName: "p2", reference: .branch("main")) - .save(on: app.db) - try await Search.refresh(on: app.db) - - do { - // MUT - let res = try await Search.fetch(app.db, ["test", "platform:ios"], page: 1, pageSize: 20) - - // validate - XCTAssertEqual(res.results.compactMap(\.packageResult?.repositoryName), ["1"]) + @Test func keywordSearchFilter() async throws { + try await withApp { app in + // Setup + let p1 = Package(url: "1", platformCompatibility: [.iOS]) + let p2 = Package(url: "2", platformCompatibility: [.macOS]) + try await [p1, p2].save(on: app.db) + try await Repository(package: p1, + defaultBranch: "main", + keywords: ["kw1", "kw2"], + name: "1", + owner: "foo", + summary: "test package").save(on: app.db) + try await Repository(package: p2, + defaultBranch: "main", + keywords: ["kw1-2"], + name: "2", + owner: "bar", + summary: "test package").save(on: app.db) + try await Version(package: p1, packageName: "p1", reference: .branch("main")) + .save(on: app.db) + try await Version(package: p2, packageName: "p2", reference: .branch("main")) + .save(on: app.db) + try await Search.refresh(on: app.db) + + do { + // MUT + let res = try await Search.fetch(app.db, ["test", "keyword:kw1"], page: 1, pageSize: 20) + + // validate + #expect(res.results.compactMap(\.packageResult?.repositoryName) == ["1"]) + } + + do { // double check that leaving the filter term off selects both packages + // MUT + let res = try await Search.fetch(app.db, ["test"], page: 1, pageSize: 20) + + // validate + #expect( + res.results.compactMap(\.packageResult?.repositoryName).sorted() == ["1", "2"] + ) + } } + } - do { // double check that leaving the filter term off selects both packages - // MUT - let res = try await Search.fetch(app.db, ["test"], page: 1, pageSize: 20) - - // validate - XCTAssertEqual( - res.results.compactMap(\.packageResult?.repositoryName).sorted(), - ["1", "2"] - ) + @Test func lastActivitySearchFilter() async throws { + try await withApp { app in + // Setup + let p1 = Package(url: "1", platformCompatibility: [.iOS]) + let p2 = Package(url: "2", platformCompatibility: [.macOS]) + try await [p1, p2].save(on: app.db) + try await Repository(package: p1, + defaultBranch: "main", + lastCommitDate: .t0.adding(days: -1), + name: "1", + owner: "foo", + summary: "test package").save(on: app.db) + try await Repository(package: p2, + defaultBranch: "main", + lastCommitDate: .t0, + name: "2", + owner: "bar", + summary: "test package").save(on: app.db) + try await Version(package: p1, packageName: "p1", reference: .branch("main")) + .save(on: app.db) + try await Version(package: p2, packageName: "p2", reference: .branch("main")) + .save(on: app.db) + try await Search.refresh(on: app.db) + + do { + // MUT + let res = try await Search.fetch(app.db, ["test", "last_activity:<1970-01-01"], page: 1, pageSize: 20) + + // validate + #expect(res.results.compactMap(\.packageResult?.repositoryName) == ["1"]) + } + + do { // double check that leaving the filter term off selects both packages + // MUT + let res = try await Search.fetch(app.db, ["test"], page: 1, pageSize: 20) + + // validate + #expect( + res.results.compactMap(\.packageResult?.repositoryName).sorted() == ["1", "2"] + ) + } } } - func test_starsSearchFilter() async throws { - // Setup - let p1 = Package(url: "1", platformCompatibility: [.iOS]) - let p2 = Package(url: "2", platformCompatibility: [.macOS]) - try await [p1, p2].save(on: app.db) - try await Repository(package: p1, - defaultBranch: "main", - name: "1", - owner: "foo", - stars: 10, - summary: "test package").save(on: app.db) - try await Repository(package: p2, - defaultBranch: "main", - name: "2", - owner: "bar", - summary: "test package").save(on: app.db) - try await Version(package: p1, packageName: "p1", reference: .branch("main")) - .save(on: app.db) - try await Version(package: p2, packageName: "p2", reference: .branch("main")) - .save(on: app.db) - try await Search.refresh(on: app.db) - - do { - // MUT - let res = try await Search.fetch(app.db, ["test", "stars:>5"], page: 1, pageSize: 20) - - // validate - XCTAssertEqual(res.results.compactMap(\.packageResult?.repositoryName), ["1"]) + @Test func lastCommitSearchFilter() async throws { + try await withApp { app in + // Setup + let p1 = Package(url: "1", platformCompatibility: [.iOS]) + let p2 = Package(url: "2", platformCompatibility: [.macOS]) + try await [p1, p2].save(on: app.db) + try await Repository(package: p1, + defaultBranch: "main", + lastCommitDate: .t0.adding(days: -1), + name: "1", + owner: "foo", + summary: "test package").save(on: app.db) + try await Repository(package: p2, + defaultBranch: "main", + lastCommitDate: .t0, + name: "2", + owner: "bar", + summary: "test package").save(on: app.db) + try await Version(package: p1, packageName: "p1", reference: .branch("main")) + .save(on: app.db) + try await Version(package: p2, packageName: "p2", reference: .branch("main")) + .save(on: app.db) + try await Search.refresh(on: app.db) + + do { + // MUT + let res = try await Search.fetch(app.db, ["test", "last_commit:<1970-01-01"], page: 1, pageSize: 20) + + // validate + #expect(res.results.compactMap(\.packageResult?.repositoryName) == ["1"]) + } + + do { // double check that leaving the filter term off selects both packages + // MUT + let res = try await Search.fetch(app.db, ["test"], page: 1, pageSize: 20) + + // validate + #expect( + res.results.compactMap(\.packageResult?.repositoryName).sorted() == ["1", "2"] + ) + } } + } - do { // double check that leaving the filter term off selects both packages - // MUT - let res = try await Search.fetch(app.db, ["test"], page: 1, pageSize: 20) - - // validate - XCTAssertEqual( - res.results.compactMap(\.packageResult?.repositoryName).sorted(), - ["1", "2"] - ) + @Test func licenseSearchFilter() async throws { + try await withApp { app in + // Setup + let p1 = Package(url: "1", platformCompatibility: [.iOS]) + let p2 = Package(url: "2", platformCompatibility: [.macOS]) + try await [p1, p2].save(on: app.db) + try await Repository(package: p1, + defaultBranch: "main", + license: .mit, + name: "1", + owner: "foo", + summary: "test package").save(on: app.db) + try await Repository(package: p2, + defaultBranch: "main", + license: .none, + name: "2", + owner: "bar", + summary: "test package").save(on: app.db) + try await Version(package: p1, packageName: "p1", reference: .branch("main")) + .save(on: app.db) + try await Version(package: p2, packageName: "p2", reference: .branch("main")) + .save(on: app.db) + try await Search.refresh(on: app.db) + + do { + // MUT + let res = try await Search.fetch(app.db, ["test", "license:mit"], page: 1, pageSize: 20) + + // validate + #expect(res.results.compactMap(\.packageResult?.repositoryName) == ["1"]) + } + + do { + // MUT + let res = try await Search.fetch(app.db, ["test", "license:compatible"], page: 1, pageSize: 20) + + // validate + #expect(res.results.compactMap(\.packageResult?.repositoryName) == ["1"]) + } + + do { // double check that leaving the filter term off selects both packages + // MUT + let res = try await Search.fetch(app.db, ["test"], page: 1, pageSize: 20) + + // validate + #expect( + res.results.compactMap(\.packageResult?.repositoryName).sorted() == ["1", "2"] + ) + } } } - func test_productTypeFilter() async throws { - // setup - do { - let p1 = Package.init(id: .id0, url: "1".url) - try await p1.save(on: app.db) + @Test func platformSearchFilter() async throws { + try await withApp { app in + // Setup + let p1 = Package(url: "1", platformCompatibility: [.iOS]) + let p2 = Package(url: "2", platformCompatibility: [.macOS]) + try await [p1, p2].save(on: app.db) try await Repository(package: p1, defaultBranch: "main", name: "1", owner: "foo", - stars: 1, summary: "test package").save(on: app.db) - let v = try Version(package: p1) - try await v.save(on: app.db) - try await Product(version: v, type: .library(.automatic), name: "lib").save(on: app.db) - } - do { - let p2 = Package.init(id: .id1, url: "2".url) - try await p2.save(on: app.db) try await Repository(package: p2, defaultBranch: "main", name: "2", owner: "foo", summary: "test package").save(on: app.db) - let v = try Version(package: p2) - try await v.save(on: app.db) - try await Product(version: v, type: .plugin, name: "plugin").save(on: app.db) + try await Version(package: p1, packageName: "p1", reference: .branch("main")) + .save(on: app.db) + try await Version(package: p2, packageName: "p2", reference: .branch("main")) + .save(on: app.db) + try await Search.refresh(on: app.db) + + do { + // MUT + let res = try await Search.fetch(app.db, ["test", "platform:ios"], page: 1, pageSize: 20) + + // validate + #expect(res.results.compactMap(\.packageResult?.repositoryName) == ["1"]) + } + + do { // double check that leaving the filter term off selects both packages + // MUT + let res = try await Search.fetch(app.db, ["test"], page: 1, pageSize: 20) + + // validate + #expect( + res.results.compactMap(\.packageResult?.repositoryName).sorted() == ["1", "2"] + ) + } } - try await Search.refresh(on: app.db) - - do { - // MUT - let res = try await Search.fetch(app.db, ["test", "product:plugin"], page: 1, pageSize: 20) + } - // validate - XCTAssertEqual(res.results.count, 1) - XCTAssertEqual( - res.results.compactMap(\.packageResult?.repositoryName), ["2"] - ) + @Test func starsSearchFilter() async throws { + try await withApp { app in + // Setup + let p1 = Package(url: "1", platformCompatibility: [.iOS]) + let p2 = Package(url: "2", platformCompatibility: [.macOS]) + try await [p1, p2].save(on: app.db) + try await Repository(package: p1, + defaultBranch: "main", + name: "1", + owner: "foo", + stars: 10, + summary: "test package").save(on: app.db) + try await Repository(package: p2, + defaultBranch: "main", + name: "2", + owner: "bar", + summary: "test package").save(on: app.db) + try await Version(package: p1, packageName: "p1", reference: .branch("main")) + .save(on: app.db) + try await Version(package: p2, packageName: "p2", reference: .branch("main")) + .save(on: app.db) + try await Search.refresh(on: app.db) + + do { + // MUT + let res = try await Search.fetch(app.db, ["test", "stars:>5"], page: 1, pageSize: 20) + + // validate + #expect(res.results.compactMap(\.packageResult?.repositoryName) == ["1"]) + } + + do { // double check that leaving the filter term off selects both packages + // MUT + let res = try await Search.fetch(app.db, ["test"], page: 1, pageSize: 20) + + // validate + #expect( + res.results.compactMap(\.packageResult?.repositoryName).sorted() == ["1", "2"] + ) + } } + } - do { - // MUT - let res = try await Search.fetch(app.db, ["test"], page: 1, pageSize: 20) + @Test func productTypeFilter() async throws { + try await withApp { app in + // setup + do { + let p1 = Package.init(id: .id0, url: "1".url) + try await p1.save(on: app.db) + try await Repository(package: p1, + defaultBranch: "main", + name: "1", + owner: "foo", + stars: 1, + summary: "test package").save(on: app.db) + let v = try Version(package: p1) + try await v.save(on: app.db) + try await Product(version: v, type: .library(.automatic), name: "lib").save(on: app.db) + } + do { + let p2 = Package.init(id: .id1, url: "2".url) + try await p2.save(on: app.db) + try await Repository(package: p2, + defaultBranch: "main", + name: "2", + owner: "foo", + summary: "test package").save(on: app.db) + let v = try Version(package: p2) + try await v.save(on: app.db) + try await Product(version: v, type: .plugin, name: "plugin").save(on: app.db) + } + try await Search.refresh(on: app.db) + + do { + // MUT + let res = try await Search.fetch(app.db, ["test", "product:plugin"], page: 1, pageSize: 20) + + // validate + #expect(res.results.count == 1) + #expect( + res.results.compactMap(\.packageResult?.repositoryName) == ["2"] + ) + } + + do { + // MUT + let res = try await Search.fetch(app.db, ["test"], page: 1, pageSize: 20) + + // validate + #expect(res.results.count == 2) + #expect( + res.results.compactMap(\.packageResult?.repositoryName) == ["1", "2"] + ) + } + } + } - // validate - XCTAssertEqual(res.results.count, 2) - XCTAssertEqual( - res.results.compactMap(\.packageResult?.repositoryName), ["1", "2"] - ) + @Test func productTypeFilter_macro() async throws { + try await withApp { app in + // setup + do { + let p1 = Package.init(id: .id0, url: "1".url) + try await p1.save(on: app.db) + try await Repository(package: p1, + defaultBranch: "main", + name: "1", + owner: "foo", + stars: 1, + summary: "test package").save(on: app.db) + let v = try Version(package: p1) + try await v.save(on: app.db) + try await Target(version: v, name: "t1", type: .regular).save(on: app.db) + } + do { + let p2 = Package.init(id: .id1, url: "2".url) + try await p2.save(on: app.db) + try await Repository(package: p2, + defaultBranch: "main", + name: "2", + owner: "foo", + summary: "test package").save(on: app.db) + let v = try Version(package: p2) + try await v.save(on: app.db) + try await Target(version: v, name: "t2", type: .macro).save(on: app.db) + } + try await Search.refresh(on: app.db) + + do { + // MUT + let res = try await Search.fetch(app.db, ["test", "product:macro"], page: 1, pageSize: 20) + + // validate + #expect(res.results.count == 1) + #expect( + res.results.compactMap(\.packageResult?.repositoryName) == ["2"] + ) + } + + do { + // MUT + let res = try await Search.fetch(app.db, ["test"], page: 1, pageSize: 20) + + // validate + #expect(res.results.count == 2) + #expect( + res.results.compactMap(\.packageResult?.repositoryName) == ["1", "2"] + ) + } } } - func test_productTypeFilter_macro() async throws { - // setup - do { - let p1 = Package.init(id: .id0, url: "1".url) - try await p1.save(on: app.db) + @Test func SearchFilter_error() async throws { + // Test error handling in case of an invalid filter + try await withApp { app in + // Setup + let p1 = Package(url: "1", platformCompatibility: [.iOS]) + let p2 = Package(url: "2", platformCompatibility: [.macOS]) + try await [p1, p2].save(on: app.db) try await Repository(package: p1, defaultBranch: "main", + license: .mit, name: "1", owner: "foo", - stars: 1, summary: "test package").save(on: app.db) - let v = try Version(package: p1) - try await v.save(on: app.db) - try await Target(version: v, name: "t1", type: .regular).save(on: app.db) - } - do { - let p2 = Package.init(id: .id1, url: "2".url) - try await p2.save(on: app.db) try await Repository(package: p2, defaultBranch: "main", + license: .none, name: "2", - owner: "foo", + owner: "bar", summary: "test package").save(on: app.db) - let v = try Version(package: p2) - try await v.save(on: app.db) - try await Target(version: v, name: "t2", type: .macro).save(on: app.db) - } - try await Search.refresh(on: app.db) + try await Version(package: p1, packageName: "p1", reference: .branch("main")) + .save(on: app.db) + try await Version(package: p2, packageName: "p2", reference: .branch("main")) + .save(on: app.db) + try await Search.refresh(on: app.db) - do { // MUT - let res = try await Search.fetch(app.db, ["test", "product:macro"], page: 1, pageSize: 20) + let res = try await Search.fetch(app.db, ["test", "license:>mit"], page: 1, pageSize: 20) // validate - XCTAssertEqual(res.results.count, 1) - XCTAssertEqual( - res.results.compactMap(\.packageResult?.repositoryName), ["2"] - ) + #expect(res.results.compactMap(\.packageResult?.repositoryName) == []) } - - do { - // MUT - let res = try await Search.fetch(app.db, ["test"], page: 1, pageSize: 20) - - // validate - XCTAssertEqual(res.results.count, 2) - XCTAssertEqual( - res.results.compactMap(\.packageResult?.repositoryName), ["1", "2"] - ) - } - } - - func test_SearchFilter_error() async throws { - // Test error handling in case of an invalid filter - // Setup - let p1 = Package(url: "1", platformCompatibility: [.iOS]) - let p2 = Package(url: "2", platformCompatibility: [.macOS]) - try await [p1, p2].save(on: app.db) - try await Repository(package: p1, - defaultBranch: "main", - license: .mit, - name: "1", - owner: "foo", - summary: "test package").save(on: app.db) - try await Repository(package: p2, - defaultBranch: "main", - license: .none, - name: "2", - owner: "bar", - summary: "test package").save(on: app.db) - try await Version(package: p1, packageName: "p1", reference: .branch("main")) - .save(on: app.db) - try await Version(package: p2, packageName: "p2", reference: .branch("main")) - .save(on: app.db) - try await Search.refresh(on: app.db) - - // MUT - let res = try await Search.fetch(app.db, ["test", "license:>mit"], page: 1, pageSize: 20) - - // validate - XCTAssertEqual(res.results.compactMap(\.packageResult?.repositoryName), []) } - func test_hasDocs_external_docs() async throws { + @Test func hasDocs_external_docs() async throws { // Ensure external docs as listed as having docs // https://github.com/SwiftPackageIndex/SwiftPackageIndex-Server/issues/2702 - let pkg = Package(url: "1") - try await pkg.save(on: app.db) - try await Repository(package: pkg, - defaultBranch: "main", - name: "1", - owner: "foo").save(on: app.db) - try await Version(package: pkg, - commit: "sha", - commitDate: .t0, - packageName: "1", - reference: .branch("main"), - spiManifest: .init(externalLinks: .init(documentation: "doc link"))) + try await withApp { app in + let pkg = Package(url: "1") + try await pkg.save(on: app.db) + try await Repository(package: pkg, + defaultBranch: "main", + name: "1", + owner: "foo").save(on: app.db) + try await Version(package: pkg, + commit: "sha", + commitDate: .t0, + packageName: "1", + reference: .branch("main"), + spiManifest: .init(externalLinks: .init(documentation: "doc link"))) .save(on: app.db) - try await Search.refresh(on: app.db) + try await Search.refresh(on: app.db) - // MUT - let res = try await Search.fetch(app.db, ["1"], page: 1, pageSize: 20) + // MUT + let res = try await Search.fetch(app.db, ["1"], page: 1, pageSize: 20) - // validate - XCTAssertEqual(res.results.first?.package?.hasDocs, true) + // validate + #expect(res.results.first?.package?.hasDocs == true) + } } - + } From d390bc65ab372720357128a6215c1ae80dae0001 Mon Sep 17 00:00:00 2001 From: "Sven A. Schmidt" Date: Tue, 4 Mar 2025 12:00:16 +0100 Subject: [PATCH 5/5] Bring back comments --- Tests/AppTests/SearchFilterTests.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Tests/AppTests/SearchFilterTests.swift b/Tests/AppTests/SearchFilterTests.swift index 6ee1b264a..b1e5d389a 100644 --- a/Tests/AppTests/SearchFilterTests.swift +++ b/Tests/AppTests/SearchFilterTests.swift @@ -94,6 +94,8 @@ import Testing #expect(app.db.renderSQL(filter.leftHandSide) == #""repo_owner""#) #expect(app.db.renderSQL(filter.sqlOperator) == "ILIKE") #expect(app.db.binds(filter.rightHandSide) == ["sherlouk"]) + + // test error case #expect { try AuthorSearchFilter(expression: .init(operator: .greaterThan, value: "sherlouk")) } throws: { @@ -119,6 +121,8 @@ import Testing #expect(app.db.binds(filter.leftHandSide) == ["cache"]) #expect(app.db.renderSQL(filter.sqlOperator) == "ILIKE") #expect(app.db.renderSQL(filter.rightHandSide) == #"ANY("keywords")"#) + + // test error case #expect { try KeywordSearchFilter(expression: .init(operator: .greaterThan, value: "cache")) } throws: { @@ -143,6 +147,8 @@ import Testing #expect(app.db.renderSQL(filter.leftHandSide) == #""last_activity_at""#) #expect(app.db.renderSQL(filter.sqlOperator) == "=") #expect(app.db.binds(filter.rightHandSide) == ["1970-01-01"]) + + // test error case #expect { try LastActivitySearchFilter(expression: .init(operator: .greaterThan, value: "23rd June 2021")) } throws: { @@ -167,6 +173,8 @@ import Testing #expect(app.db.renderSQL(filter.leftHandSide) == #""last_commit_date""#) #expect(app.db.renderSQL(filter.sqlOperator) == "=") #expect(app.db.binds(filter.rightHandSide) == ["1970-01-01"]) + + // test error case #expect { try LastCommitSearchFilter(expression: .init(operator: .greaterThan, value: "23rd June 2021")) } throws: { @@ -396,6 +404,8 @@ import Testing #expect(app.db.renderSQL(filter.leftHandSide) == #""stars""#) #expect(app.db.renderSQL(filter.sqlOperator) == "=") #expect(app.db.binds(filter.rightHandSide) == ["1234"]) + + // test error case #expect { try StarsSearchFilter(expression: .init(operator: .greaterThan, value: "one")) } throws: {