Files
ShadowsocksX-NG/ShadowsocksX-NG2/Tests/Rules/OfflineRulesWorkflowTests.swift
Qiu Yuzhou f6abd4343e feat(rules)!: add isolated routing presets
Add Mainland China proxy routing with independent rule workspaces.
Preserve existing routing choices through equivalent preset mappings.

BREAKING CHANGE: existing custom rules and enablement settings are
not migrated. Recreate them in the appropriate preset.
2026-10-25 14:32:12 +08:00

105 lines
4.8 KiB
Swift

import XCTest
@testable import ShadowsocksX_NG2
@MainActor
final class OfflineRulesWorkflowTests: XCTestCase {
func testAddressTestUsesCompleteSavedCollectionDespiteBrowsingFilters() async throws {
let rule = CustomRule(action: .proxy, match: try RuleMatch(domainSuffix: "example.com"))
let workflow = RulesWorkflow(
preset: .gfwlist, loadCustom: { [rule] },
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
await workflow.refresh()
workflow.query(RulesQuery(search: "hidden", action: .direct, source: .chinaIPv4))
XCTAssertTrue(workflow.snapshot.rows.isEmpty)
workflow.setTestTarget("a.example.com")
await workflow.testAddress()
let result = try XCTUnwrap(workflow.snapshot.addressTest.result)
XCTAssertEqual(result.outcome, .proxy)
XCTAssertEqual(result.version, workflow.snapshot.version)
XCTAssertEqual(result.deciding.map(\.identity), [rule.identity])
XCTAssertTrue(result.domainWithoutDNS)
}
func testInvalidAndIncompleteCollectionsExposeOperationErrors() async throws {
let workflow = RulesWorkflow(
preset: .gfwlist, loadCustom: { [] },
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
workflow.setTestTarget("example.com")
await workflow.testAddress()
XCTAssertEqual(workflow.snapshot.addressTest.failure, .incompleteCollection)
await workflow.refresh()
workflow.setTestTarget("https://")
await workflow.testAddress()
XCTAssertEqual(workflow.snapshot.addressTest.failure, .invalidTarget)
XCTAssertNil(workflow.snapshot.addressTest.result)
let incomplete = RulesWorkflow(
preset: .gfwlist, loadCustom: { [] },
builtinSnapshots: BuiltinRuleSnapshots(loader: { _ in throw RuleSnapshotError.missing }))
await incomplete.refresh()
incomplete.setTestTarget("localhost")
await incomplete.testAddress()
XCTAssertEqual(incomplete.snapshot.addressTest.failure, .incompleteCollection)
XCTAssertNil(incomplete.snapshot.addressTest.result)
}
func testPublicRefreshClearsOldResultAndUsesNewSavedVersion() async throws {
let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
defer { try? FileManager.default.removeItem(at: directory) }
let store = CustomRuleStore(fileURL: directory.appendingPathComponent("rules.json"))
let proxy = CustomRule(action: .proxy, match: try RuleMatch(domainExact: "example.com"))
try store.save([proxy])
let workflow = RulesWorkflow(
preset: .gfwlist, loadCustom: { try store.load() },
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
await workflow.refresh()
workflow.setTestTarget("example.com")
await workflow.testAddress()
let version = workflow.snapshot.addressTest.result?.version
try store.save([CustomRule(action: .direct, match: proxy.match)])
await workflow.refresh()
XCTAssertNil(workflow.snapshot.addressTest.result)
XCTAssertFalse(workflow.snapshot.addressTest.isTesting)
XCTAssertNotEqual(workflow.snapshot.version, version)
XCTAssertEqual(workflow.snapshot.addressTest.target, "example.com")
await workflow.testAddress()
XCTAssertEqual(workflow.snapshot.addressTest.result?.outcome, .direct)
try Data("broken".utf8).write(to: store.fileURL)
await workflow.refresh()
await workflow.testAddress()
XCTAssertEqual(workflow.snapshot.addressTest.failure, .incompleteCollection)
XCTAssertNil(workflow.snapshot.addressTest.result)
}
func testClearingTargetOrStartingRefreshPreventsLateTestPublication() async throws {
let rule = CustomRule(action: .proxy, match: try RuleMatch(domainSuffix: "example.com"))
let workflow = RulesWorkflow(
preset: .gfwlist, loadCustom: { [rule] },
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
await workflow.refresh()
workflow.setTestTarget("example.com")
await workflow.testAddress()
workflow.setTestTarget("")
XCTAssertNil(workflow.snapshot.addressTest.result)
XCTAssertNil(workflow.snapshot.addressTest.failure)
// Queue a new public command as soon as the old test starts. The old
// detached task must not publish after that command invalidates its intent.
for refresh in [false, true] {
workflow.setTestTarget("example.com")
var invalidation: Task<Void, Never>?
let observation = workflow.$snapshot.sink { page in
guard page.addressTest.isTesting, invalidation == nil else { return }
invalidation = Task { @MainActor in
if refresh { await workflow.refresh() } else { workflow.setTestTarget("") }
}
}
await workflow.testAddress()
await invalidation?.value
observation.cancel()
XCTAssertNil(workflow.snapshot.addressTest.result)
XCTAssertFalse(workflow.snapshot.addressTest.isTesting)
}
}
}