mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-02 08:33:46 +00:00
refactor(rules): 统一来源类型映射并移除测试专用加载参数
This commit is contained in:
@@ -31,15 +31,7 @@ final class RulesWorkflow: ObservableObject {
|
||||
analysisDelay: @escaping @Sendable () async throws -> Void = {
|
||||
try await Task.sleep(for: .milliseconds(150))
|
||||
},
|
||||
builtinSnapshots: BuiltinRuleSnapshots? = nil,
|
||||
loadBuiltin: @escaping @Sendable (RulesSource) throws -> RuleSnapshot = { source in
|
||||
switch source {
|
||||
case .geolocationCN: try BuiltinRuleCatalog.loadGeolocationCN()
|
||||
case .chinaIPv4: try BuiltinRuleCatalog.loadChinaIPv4()
|
||||
case .gfwlist: try BuiltinRuleCatalog.loadGFWList()
|
||||
case .custom, .fixed: throw RuleSnapshotError.missing
|
||||
}
|
||||
}
|
||||
builtinSnapshots: BuiltinRuleSnapshots = BuiltinRuleSnapshots()
|
||||
) {
|
||||
if let loadDocument {
|
||||
self.loadDocument = loadDocument
|
||||
@@ -52,7 +44,7 @@ final class RulesWorkflow: ObservableObject {
|
||||
self.feedbackDelay = feedbackDelay
|
||||
self.analysisDelay = analysisDelay
|
||||
self.loadCustom = loadCustom ?? { try CustomRuleStore().load() }
|
||||
self.builtinSnapshots = builtinSnapshots ?? BuiltinRuleSnapshots(loader: loadBuiltin)
|
||||
self.builtinSnapshots = builtinSnapshots
|
||||
}
|
||||
|
||||
func refresh(retryFailedSources: Bool = false) async {
|
||||
|
||||
@@ -19,7 +19,9 @@ struct BuiltinRuleCatalog {
|
||||
try loadSnapshot(named: "gfwlist", from: bundle)
|
||||
}
|
||||
|
||||
private static func loadSnapshot(named name: String, from bundle: Bundle) throws -> RuleSnapshot {
|
||||
fileprivate static func loadSnapshot(named name: String, from bundle: Bundle) throws
|
||||
-> RuleSnapshot
|
||||
{
|
||||
// folder reference 保留 `rules/<source>/snapshot.json` 目录结构。
|
||||
guard
|
||||
let url = bundle.url(
|
||||
@@ -62,12 +64,10 @@ actor BuiltinRuleSnapshots {
|
||||
init(bundle: Bundle = .main, loader: Loader? = nil) {
|
||||
self.loader =
|
||||
loader ?? { source in
|
||||
switch source {
|
||||
case .geolocationCN: try BuiltinRuleCatalog.loadGeolocationCN(from: bundle)
|
||||
case .chinaIPv4: try BuiltinRuleCatalog.loadChinaIPv4(from: bundle)
|
||||
case .gfwlist: try BuiltinRuleCatalog.loadGFWList(from: bundle)
|
||||
case .fixed, .custom: throw RuleSnapshotError.missing
|
||||
guard let kind = source.sourceKind, kind != .custom else {
|
||||
throw RuleSnapshotError.missing
|
||||
}
|
||||
return try BuiltinRuleCatalog.loadSnapshot(named: kind.rawValue, from: bundle)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,12 +82,8 @@ actor BuiltinRuleSnapshots {
|
||||
id: UUID(),
|
||||
task: Task.detached(priority: .userInitiated) {
|
||||
let snapshot = try loader(source)
|
||||
let kind: RuleSourceKind
|
||||
switch source {
|
||||
case .geolocationCN: kind = .geolocationCN
|
||||
case .chinaIPv4: kind = .chinaIPv4
|
||||
case .gfwlist: kind = .gfwlist
|
||||
case .custom, .fixed: throw RuleSnapshotError.missing
|
||||
guard let kind = source.sourceKind, kind != .custom else {
|
||||
throw RuleSnapshotError.missing
|
||||
}
|
||||
guard snapshot.metadata.source.kind == kind
|
||||
else { throw RuleSnapshotError.corrupt(detail: "Unexpected rule source") }
|
||||
|
||||
@@ -11,6 +11,17 @@ struct RulesQuery: Equatable, Sendable {
|
||||
enum RulesSource: String, CaseIterable, Identifiable, Sendable {
|
||||
case custom, geolocationCN, chinaIPv4, gfwlist, fixed
|
||||
var id: Self { self }
|
||||
|
||||
/// Fixed policy has no snapshot metadata kind.
|
||||
var sourceKind: RuleSourceKind? {
|
||||
switch self {
|
||||
case .custom: .custom
|
||||
case .geolocationCN: .geolocationCN
|
||||
case .chinaIPv4: .chinaIPv4
|
||||
case .gfwlist: .gfwlist
|
||||
case .fixed: nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RulesSourceSnapshot: Equatable, Identifiable, Sendable {
|
||||
|
||||
@@ -89,13 +89,7 @@ private struct RulesCollectionInput: Sendable {
|
||||
) {
|
||||
do {
|
||||
let snapshot = try loadBuiltin(source)
|
||||
let expected: RuleSourceKind
|
||||
switch source {
|
||||
case .geolocationCN: expected = .geolocationCN
|
||||
case .chinaIPv4: expected = .chinaIPv4
|
||||
case .gfwlist: expected = .gfwlist
|
||||
case .custom, .fixed: expected = .custom
|
||||
}
|
||||
guard let expected = source.sourceKind else { throw RuleSnapshotError.missing }
|
||||
guard snapshot.metadata.source.kind == expected
|
||||
else { throw RuleSnapshotError.corrupt(detail: "Unexpected rule source") }
|
||||
let candidates = snapshot.rules
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
import XCTest
|
||||
|
||||
@testable import ShadowsocksX_NG2
|
||||
@@ -255,3 +256,25 @@ private final class SharedRuleSourceLoader: @unchecked Sendable {
|
||||
return rulesFixture(source)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func builtinSnapshotsRejectNonBuiltinSources() async {
|
||||
let snapshots = BuiltinRuleSnapshots(loader: { _ in rulesFixture(.custom) })
|
||||
for source in [RulesSource.custom, .fixed] {
|
||||
do {
|
||||
_ = try await snapshots.load(source)
|
||||
Issue.record("Non-builtin source must not load a snapshot")
|
||||
} catch {
|
||||
#expect(error as? RuleSnapshotError == .missing)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func builtinSnapshotsRejectMismatchedMetadata() async {
|
||||
let snapshots = BuiltinRuleSnapshots(loader: { _ in rulesFixture(.chinaIPv4) })
|
||||
do {
|
||||
_ = try await snapshots.load(.geolocationCN)
|
||||
Issue.record("Mismatched source metadata must be rejected")
|
||||
} catch {
|
||||
#expect(error as? RuleSnapshotError == .corrupt(detail: "Unexpected rule source"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,9 @@ final class CustomRuleDeletionWorkflowTests: XCTestCase {
|
||||
saved = $0
|
||||
return RuleDocumentCommit(outcome: .saved, document: saved)
|
||||
},
|
||||
loadBuiltin: { rulesFixture($0, rules: $0 == .gfwlist ? [builtin] : []) })
|
||||
builtinSnapshots: BuiltinRuleSnapshots(loader: {
|
||||
rulesFixture($0, rules: $0 == .gfwlist ? [builtin] : [])
|
||||
}))
|
||||
await workflow.refresh()
|
||||
workflow.select(Set(workflow.snapshot.rows.map(\.id)))
|
||||
XCTAssertEqual(workflow.deletableSelection, [custom.id, other.id])
|
||||
@@ -57,7 +59,7 @@ extension CustomRuleDeletionWorkflowTests {
|
||||
commits += 1
|
||||
saved = $0
|
||||
return RuleDocumentCommit(outcome: .saved, document: saved)
|
||||
}, loadBuiltin: { rulesFixture($0) })
|
||||
}, builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
|
||||
await workflow.refresh()
|
||||
workflow.select(Set(workflow.snapshot.rows.map(\.id)))
|
||||
var query = workflow.snapshot.query
|
||||
@@ -86,7 +88,7 @@ extension CustomRuleDeletionWorkflowTests {
|
||||
commitDocument: {
|
||||
commits += 1
|
||||
return RuleDocumentCommit(outcome: .saved, document: $0)
|
||||
}, loadBuiltin: { rulesFixture($0) })
|
||||
}, builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
|
||||
await workflow.refresh()
|
||||
workflow.select(Set(workflow.snapshot.rows.map(\.id)))
|
||||
let confirmation = try XCTUnwrap(workflow.prepareCustomRuleDeletion())
|
||||
@@ -114,7 +116,7 @@ extension CustomRuleDeletionWorkflowTests {
|
||||
started.fulfill()
|
||||
}
|
||||
return RuleDocumentCommit(outcome: .persistenceFailed, document: original)
|
||||
}, loadBuiltin: { rulesFixture($0) })
|
||||
}, builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
|
||||
await workflow.refresh()
|
||||
workflow.select(Set(workflow.snapshot.rows.map(\.id)))
|
||||
let confirmation = try XCTUnwrap(workflow.prepareCustomRuleDeletion())
|
||||
@@ -143,7 +145,7 @@ extension CustomRuleDeletionWorkflowTests {
|
||||
commitDocument: {
|
||||
commits += 1
|
||||
return RuleDocumentCommit(outcome: .saved, document: $0)
|
||||
}, loadBuiltin: { rulesFixture($0) })
|
||||
}, builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
|
||||
await workflow.refresh()
|
||||
workflow.select(Set(workflow.snapshot.rows.map(\.id)))
|
||||
let confirmation = try XCTUnwrap(workflow.prepareCustomRuleDeletion())
|
||||
|
||||
@@ -8,7 +8,7 @@ final class CustomRuleEditingWorkflowTests: XCTestCase {
|
||||
let workflow = RulesWorkflow(
|
||||
loadDocument: { CustomRuleDocument(rules: []) },
|
||||
commitDocument: { RuleDocumentCommit(outcome: .saved, document: $0) },
|
||||
loadBuiltin: { rulesFixture($0) })
|
||||
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
|
||||
await workflow.refresh()
|
||||
let version = workflow.snapshot.version
|
||||
var draft = try XCTUnwrap(workflow.makeCustomRuleDraft())
|
||||
@@ -40,7 +40,9 @@ final class CustomRuleEditingWorkflowTests: XCTestCase {
|
||||
saved = $0
|
||||
return RuleDocumentCommit(outcome: .saved, document: saved)
|
||||
},
|
||||
loadBuiltin: { rulesFixture($0, rules: $0 == .gfwlist ? [builtin] : []) })
|
||||
builtinSnapshots: BuiltinRuleSnapshots(loader: {
|
||||
rulesFixture($0, rules: $0 == .gfwlist ? [builtin] : [])
|
||||
}))
|
||||
await workflow.refresh()
|
||||
var draft = try XCTUnwrap(workflow.makeCustomRuleDraft(editing: original.id))
|
||||
let unchanged = await workflow.previewCustomRule(draft)
|
||||
@@ -71,7 +73,7 @@ final class CustomRuleEditingWorkflowTests: XCTestCase {
|
||||
let workflow = RulesWorkflow(
|
||||
loadDocument: { CustomRuleDocument(rules: []) },
|
||||
commitDocument: { RuleDocumentCommit(outcome: .saved, document: $0) },
|
||||
loadBuiltin: { rulesFixture($0) })
|
||||
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
|
||||
await workflow.refresh()
|
||||
var draft = try XCTUnwrap(workflow.makeCustomRuleDraft())
|
||||
draft.kind = .cidr
|
||||
@@ -100,7 +102,7 @@ final class CustomRuleEditingWorkflowTests: XCTestCase {
|
||||
commits += 1
|
||||
return RuleDocumentCommit(outcome: .saved, document: $0)
|
||||
},
|
||||
loadBuiltin: { rulesFixture($0) })
|
||||
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
|
||||
await workflow.refresh()
|
||||
var draft = try XCTUnwrap(workflow.makeCustomRuleDraft())
|
||||
draft.kind = .ipAddress
|
||||
@@ -134,7 +136,9 @@ final class CustomRuleEditingWorkflowTests: XCTestCase {
|
||||
CustomRuleDocument(rules: [], disabledIdentities: [builtin.identity, orphan])
|
||||
},
|
||||
commitDocument: { RuleDocumentCommit(outcome: .saved, document: $0) },
|
||||
loadBuiltin: { rulesFixture($0, rules: $0 == .gfwlist ? [builtin] : []) })
|
||||
builtinSnapshots: BuiltinRuleSnapshots(loader: {
|
||||
rulesFixture($0, rules: $0 == .gfwlist ? [builtin] : [])
|
||||
}))
|
||||
await workflow.refresh()
|
||||
var draft = try XCTUnwrap(workflow.makeCustomRuleDraft())
|
||||
draft.content = ".MERGE.Example"
|
||||
@@ -163,7 +167,7 @@ final class CustomRuleEditingWorkflowTests: XCTestCase {
|
||||
let workflow = RulesWorkflow(
|
||||
loadDocument: { CustomRuleDocument(rules: []) },
|
||||
commitDocument: { RuleDocumentCommit(outcome: .saved, document: $0) },
|
||||
loadBuiltin: { rulesFixture($0) })
|
||||
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
|
||||
await workflow.refresh()
|
||||
var draft = try XCTUnwrap(workflow.makeCustomRuleDraft())
|
||||
draft.action = .direct
|
||||
@@ -200,7 +204,7 @@ final class CustomRuleEditingWorkflowTests: XCTestCase {
|
||||
let workflow = RulesWorkflow(
|
||||
loadDocument: { CustomRuleDocument(rules: [broad, proxy]) },
|
||||
commitDocument: { RuleDocumentCommit(outcome: .saved, document: $0) },
|
||||
loadBuiltin: { rulesFixture($0) })
|
||||
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
|
||||
await workflow.refresh()
|
||||
var draft = try XCTUnwrap(workflow.makeCustomRuleDraft())
|
||||
draft.kind = .domainExact
|
||||
@@ -239,7 +243,7 @@ final class CustomRuleEditingWorkflowTests: XCTestCase {
|
||||
commits += 1
|
||||
return RuleDocumentCommit(outcome: .saved, document: $0)
|
||||
},
|
||||
loadBuiltin: { rulesFixture($0) })
|
||||
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
|
||||
await workflow.refresh()
|
||||
var discarded = try XCTUnwrap(workflow.makeCustomRuleDraft(editing: rule.id))
|
||||
discarded.content = "discarded.example"
|
||||
@@ -274,7 +278,7 @@ extension CustomRuleEditingWorkflowTests {
|
||||
}
|
||||
return RuleDocumentCommit(outcome: .persistenceFailed, document: original)
|
||||
},
|
||||
loadBuiltin: { rulesFixture($0) })
|
||||
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
|
||||
await workflow.refresh()
|
||||
let version = workflow.snapshot.version
|
||||
var draft = try XCTUnwrap(workflow.makeCustomRuleDraft())
|
||||
@@ -304,7 +308,7 @@ extension CustomRuleEditingWorkflowTests {
|
||||
let workflow = RulesWorkflow(
|
||||
loadDocument: { original },
|
||||
commitDocument: { RuleDocumentCommit(outcome: outcome, document: $0) },
|
||||
loadBuiltin: { rulesFixture($0) })
|
||||
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
|
||||
XCTAssertNil(workflow.makeCustomRuleDraft())
|
||||
await workflow.refresh()
|
||||
var draft = try XCTUnwrap(workflow.makeCustomRuleDraft(editing: rule.id))
|
||||
@@ -319,7 +323,7 @@ extension CustomRuleEditingWorkflowTests {
|
||||
let incomplete = RulesWorkflow(
|
||||
loadDocument: { throw CustomRuleStoreError.corrupt(detail: "fixture error") },
|
||||
commitDocument: { RuleDocumentCommit(outcome: .saved, document: $0) },
|
||||
loadBuiltin: { rulesFixture($0) })
|
||||
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
|
||||
await incomplete.refresh()
|
||||
XCTAssertNil(incomplete.makeCustomRuleDraft())
|
||||
let refused = await incomplete.saveCustomRule(draft)
|
||||
@@ -339,7 +343,7 @@ extension CustomRuleEditingWorkflowTests {
|
||||
commits += 1
|
||||
return RuleDocumentCommit(outcome: .saved, document: $0)
|
||||
},
|
||||
loadBuiltin: { rulesFixture($0) })
|
||||
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
|
||||
await workflow.refresh()
|
||||
var draft = try XCTUnwrap(workflow.makeCustomRuleDraft(editing: original.id))
|
||||
draft.kind = .cidr
|
||||
|
||||
@@ -6,7 +6,8 @@ import XCTest
|
||||
final class OfflineRulesWorkflowTests: XCTestCase {
|
||||
func testAddressTestUsesCompleteSavedCollectionDespiteBrowsingFilters() async throws {
|
||||
let rule = CustomRule(action: .proxy, match: try RuleMatch(domainSuffix: "example.com"))
|
||||
let workflow = RulesWorkflow(loadCustom: { [rule] }, loadBuiltin: { rulesFixture($0) })
|
||||
let workflow = RulesWorkflow(
|
||||
loadCustom: { [rule] }, builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
|
||||
await workflow.refresh()
|
||||
workflow.query(RulesQuery(search: "hidden", action: .direct, source: .chinaIPv4))
|
||||
XCTAssertTrue(workflow.snapshot.rows.isEmpty)
|
||||
@@ -19,7 +20,8 @@ final class OfflineRulesWorkflowTests: XCTestCase {
|
||||
XCTAssertTrue(result.domainWithoutDNS)
|
||||
}
|
||||
func testInvalidAndIncompleteCollectionsExposeOperationErrors() async throws {
|
||||
let workflow = RulesWorkflow(loadCustom: { [] }, loadBuiltin: { rulesFixture($0) })
|
||||
let workflow = RulesWorkflow(
|
||||
loadCustom: { [] }, builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
|
||||
workflow.setTestTarget("example.com")
|
||||
await workflow.testAddress()
|
||||
XCTAssertEqual(workflow.snapshot.addressTest.failure, .incompleteCollection)
|
||||
@@ -29,7 +31,8 @@ final class OfflineRulesWorkflowTests: XCTestCase {
|
||||
XCTAssertEqual(workflow.snapshot.addressTest.failure, .invalidTarget)
|
||||
XCTAssertNil(workflow.snapshot.addressTest.result)
|
||||
let incomplete = RulesWorkflow(
|
||||
loadCustom: { [] }, loadBuiltin: { _ in throw RuleSnapshotError.missing })
|
||||
loadCustom: { [] },
|
||||
builtinSnapshots: BuiltinRuleSnapshots(loader: { _ in throw RuleSnapshotError.missing }))
|
||||
await incomplete.refresh()
|
||||
incomplete.setTestTarget("localhost")
|
||||
await incomplete.testAddress()
|
||||
@@ -44,7 +47,8 @@ final class OfflineRulesWorkflowTests: XCTestCase {
|
||||
let proxy = CustomRule(action: .proxy, match: try RuleMatch(domainExact: "example.com"))
|
||||
try store.save([proxy])
|
||||
let workflow = RulesWorkflow(
|
||||
loadCustom: { try store.load() }, loadBuiltin: { rulesFixture($0) })
|
||||
loadCustom: { try store.load() },
|
||||
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
|
||||
await workflow.refresh()
|
||||
workflow.setTestTarget("example.com")
|
||||
await workflow.testAddress()
|
||||
@@ -66,7 +70,8 @@ final class OfflineRulesWorkflowTests: XCTestCase {
|
||||
|
||||
func testClearingTargetOrStartingRefreshPreventsLateTestPublication() async throws {
|
||||
let rule = CustomRule(action: .proxy, match: try RuleMatch(domainSuffix: "example.com"))
|
||||
let workflow = RulesWorkflow(loadCustom: { [rule] }, loadBuiltin: { rulesFixture($0) })
|
||||
let workflow = RulesWorkflow(
|
||||
loadCustom: { [rule] }, builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
|
||||
await workflow.refresh()
|
||||
workflow.setTestTarget("example.com")
|
||||
await workflow.testAddress()
|
||||
@@ -102,7 +107,8 @@ final class OfflineRulesWorkflowTests: XCTestCase {
|
||||
CustomRule(action: .proxy, match: try RuleMatch(domainExact: "proxy.example")),
|
||||
CustomRule(action: .direct, match: try RuleMatch(domainExact: "direct.example")),
|
||||
]
|
||||
let workflow = RulesWorkflow(loadCustom: { rules }, loadBuiltin: { rulesFixture($0) })
|
||||
let workflow = RulesWorkflow(
|
||||
loadCustom: { rules }, builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
|
||||
await workflow.refresh()
|
||||
for mode in [ProxyModeKind.rule, .global, .direct] {
|
||||
for defaultAction in RuleDefaultAction.allCases {
|
||||
|
||||
@@ -18,7 +18,7 @@ final class RuleEnablementWorkflowTests: XCTestCase {
|
||||
return RuleDocumentCommit(outcome: .saved, document: $0)
|
||||
},
|
||||
analysisDelay: { await gate.wait() },
|
||||
loadBuiltin: { rulesFixture($0) })
|
||||
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
|
||||
await workflow.refresh()
|
||||
workflow.query(RulesQuery(source: .custom))
|
||||
let originalOrder = workflow.snapshot.rows.map(\.id)
|
||||
@@ -69,7 +69,7 @@ final class RuleEnablementWorkflowTests: XCTestCase {
|
||||
}
|
||||
},
|
||||
analysisDelay: { await gate.wait() },
|
||||
loadBuiltin: { source in
|
||||
builtinSnapshots: BuiltinRuleSnapshots(loader: { source in
|
||||
if source == .geolocationCN {
|
||||
return rulesFixture(
|
||||
source,
|
||||
@@ -79,7 +79,7 @@ final class RuleEnablementWorkflowTests: XCTestCase {
|
||||
])
|
||||
}
|
||||
return rulesFixture(source)
|
||||
})
|
||||
}))
|
||||
await workflow.refresh()
|
||||
XCTAssertTrue(workflow.snapshot.isComplete)
|
||||
XCTAssertTrue(workflow.snapshot.sources.contains { $0.id == .geolocationCN })
|
||||
@@ -109,10 +109,10 @@ final class RuleEnablementWorkflowTests: XCTestCase {
|
||||
return CustomRuleDocument(rules: [rule])
|
||||
},
|
||||
commitDocument: { RuleDocumentCommit(outcome: .saved, document: $0) },
|
||||
loadBuiltin: { source in
|
||||
builtinSnapshots: BuiltinRuleSnapshots(loader: { source in
|
||||
reads.recordSource()
|
||||
return rulesFixture(source)
|
||||
})
|
||||
}))
|
||||
await workflow.refresh()
|
||||
await workflow.setEnabled(false, identities: [rule.identity])
|
||||
XCTAssertEqual(reads.documentCount, 1)
|
||||
@@ -147,7 +147,7 @@ final class RuleEnablementWorkflowTests: XCTestCase {
|
||||
outcome: .persistenceFailed, document: try? store.loadDocument())
|
||||
}
|
||||
},
|
||||
loadBuiltin: { rulesFixture($0) })
|
||||
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
|
||||
await workflow.refresh()
|
||||
let pending = Task { await workflow.setEnabled(false, identities: [rule.identity]) }
|
||||
await fulfillment(of: [started], timeout: 3)
|
||||
@@ -181,7 +181,7 @@ final class RuleEnablementWorkflowTests: XCTestCase {
|
||||
commits += 1
|
||||
return RuleDocumentCommit(outcome: .persistenceFailed, document: try? store.loadDocument())
|
||||
},
|
||||
loadBuiltin: { rulesFixture($0) })
|
||||
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
|
||||
await workflow.refresh()
|
||||
let version = workflow.snapshot.version
|
||||
await workflow.setEnabled(false, identities: [rule.identity])
|
||||
|
||||
@@ -18,7 +18,7 @@ final class RuleStatusWorkflowTests: XCTestCase {
|
||||
return RuleDocumentCommit(outcome: .saved, document: nil)
|
||||
},
|
||||
feedbackDelay: { XCTFail("An incomplete collection must not start the success timer") },
|
||||
loadBuiltin: { rulesFixture($0) })
|
||||
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
|
||||
await workflow.refresh()
|
||||
let rows = workflow.snapshot.rows
|
||||
await workflow.setEnabled(false, identities: [rule.identity])
|
||||
@@ -46,7 +46,7 @@ final class RuleStatusWorkflowTests: XCTestCase {
|
||||
saveFixture(document, to: store, outcome: .saved)
|
||||
},
|
||||
feedbackDelay: { await clock.wait() },
|
||||
loadBuiltin: { rulesFixture($0) })
|
||||
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
|
||||
await workflow.refresh()
|
||||
let fixed = try XCTUnwrap(workflow.snapshot.rows.first { $0.isFixed }?.identity)
|
||||
await workflow.setEnabled(true, identities: [disabled.identity, enabled.identity, fixed])
|
||||
@@ -80,7 +80,7 @@ final class RuleStatusWorkflowTests: XCTestCase {
|
||||
await gate.wait()
|
||||
return saveFixture(document, to: store, outcome: .saved)
|
||||
},
|
||||
loadBuiltin: { rulesFixture($0) })
|
||||
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
|
||||
await workflow.refresh()
|
||||
let pending = Task { await workflow.setEnabled(false, identities: [rule.identity]) }
|
||||
await fulfillment(of: [reloadStarted], timeout: 3)
|
||||
@@ -121,7 +121,7 @@ final class RuleStatusWorkflowTests: XCTestCase {
|
||||
await clock.wait()
|
||||
timerReturned.fulfill()
|
||||
},
|
||||
loadBuiltin: { rulesFixture($0) })
|
||||
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
|
||||
await workflow.refresh()
|
||||
await workflow.setEnabled(false, identities: [rule.identity])
|
||||
await fulfillment(of: [timerStarted], timeout: 3)
|
||||
|
||||
@@ -6,7 +6,8 @@ import XCTest
|
||||
final class RulesPresentationTests: XCTestCase {
|
||||
func testReportIsAnExplicitSnapshotUnaffectedByQueryOrRefresh() async throws {
|
||||
let loader = ReportFixtureLoader()
|
||||
let workflow = RulesWorkflow(loadCustom: { [] }, loadBuiltin: { loader.load($0) })
|
||||
let workflow = RulesWorkflow(
|
||||
loadCustom: { [] }, builtinSnapshots: BuiltinRuleSnapshots(loader: { loader.load($0) }))
|
||||
await workflow.refresh()
|
||||
XCTAssertTrue(workflow.openSourceReport(.gfwlist))
|
||||
let opened = try XCTUnwrap(workflow.reportSource)
|
||||
@@ -26,7 +27,8 @@ final class RulesPresentationTests: XCTestCase {
|
||||
let narrow = CustomRule(action: .direct, match: try RuleMatch(domainExact: "a.example.com"))
|
||||
let independent = CustomRule(action: .direct, match: try RuleMatch(domainExact: "other.net"))
|
||||
let workflow = RulesWorkflow(
|
||||
loadCustom: { [broad, narrow, independent] }, loadBuiltin: { rulesFixture($0) })
|
||||
loadCustom: { [broad, narrow, independent] },
|
||||
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
|
||||
await workflow.refresh()
|
||||
XCTAssertNil(workflow.selectedRelationshipRow)
|
||||
workflow.select([.rule(narrow.identity)])
|
||||
@@ -45,7 +47,8 @@ final class RulesPresentationTests: XCTestCase {
|
||||
let narrow = CustomRule(action: .direct, match: try RuleMatch(domainExact: "a.example.net"))
|
||||
let local = CustomRule(action: .direct, match: try RuleMatch(domainExact: "device.local"))
|
||||
let workflow = RulesWorkflow(
|
||||
loadCustom: { [broad, narrow, local] }, loadBuiltin: { rulesFixture($0) })
|
||||
loadCustom: { [broad, narrow, local] },
|
||||
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
|
||||
await workflow.refresh()
|
||||
workflow.select([.rule(narrow.identity)])
|
||||
let absorbed = try XCTUnwrap(workflow.selectedRelationshipRow)
|
||||
|
||||
@@ -10,7 +10,8 @@ final class RulesWorkflowCoverageTests: XCTestCase {
|
||||
let duplicate = CustomRule(
|
||||
action: .direct, match: try RuleMatch(domainExact: "www.example.com"))
|
||||
let workflow = RulesWorkflow(
|
||||
loadCustom: { [broad, narrow, duplicate] }, loadBuiltin: { rulesFixture($0) })
|
||||
loadCustom: { [broad, narrow, duplicate] },
|
||||
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
|
||||
await workflow.refresh()
|
||||
workflow.query(RulesQuery(action: .direct, source: .custom))
|
||||
let broadRow = try XCTUnwrap(workflow.snapshot.rows.first { $0.identity == broad.identity })
|
||||
@@ -30,7 +31,8 @@ final class RulesWorkflowCoverageTests: XCTestCase {
|
||||
let lower = CustomRule(action: .direct, match: try RuleMatch(ipv4CIDR: "8.8.8.0/25"))
|
||||
let upper = CustomRule(action: .direct, match: try RuleMatch(ipv4CIDR: "8.8.8.128/25"))
|
||||
let workflow = RulesWorkflow(
|
||||
loadCustom: { [proxy, lower, upper] }, loadBuiltin: { rulesFixture($0) })
|
||||
loadCustom: { [proxy, lower, upper] },
|
||||
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
|
||||
await workflow.refresh()
|
||||
workflow.query(RulesQuery(action: .proxy, source: .custom))
|
||||
let row = try XCTUnwrap(workflow.snapshot.rows.first)
|
||||
@@ -51,10 +53,10 @@ final class RulesWorkflowCoverageTests: XCTestCase {
|
||||
let nationalDirect = CustomRule(action: .direct, match: .domainSuffix("cn"))
|
||||
let workflow = RulesWorkflow(
|
||||
loadCustom: { [localDirect, nationalDirect] },
|
||||
loadBuiltin: { requested in
|
||||
builtinSnapshots: BuiltinRuleSnapshots(loader: { requested in
|
||||
rulesFixture(
|
||||
requested, rules: requested == .gfwlist ? [simple, simpleSuffix, localProxy] : [])
|
||||
})
|
||||
}))
|
||||
await workflow.refresh()
|
||||
workflow.query(RulesQuery(source: .gfwlist))
|
||||
let simpleRow = try XCTUnwrap(workflow.snapshot.rows.first { $0.identity == simple.identity })
|
||||
@@ -79,9 +81,9 @@ final class RulesWorkflowCoverageTests: XCTestCase {
|
||||
let proxy = ProxyRule(action: .proxy, match: try RuleMatch(domainExact: "cn"))
|
||||
let workflow = RulesWorkflow(
|
||||
loadCustom: { [direct] },
|
||||
loadBuiltin: { requested in
|
||||
builtinSnapshots: BuiltinRuleSnapshots(loader: { requested in
|
||||
rulesFixture(requested, rules: requested == .gfwlist ? [proxy] : [])
|
||||
})
|
||||
}))
|
||||
await workflow.refresh()
|
||||
workflow.query(RulesQuery(source: .custom))
|
||||
let row = try XCTUnwrap(workflow.snapshot.rows.first)
|
||||
|
||||
@@ -5,7 +5,8 @@ import XCTest
|
||||
@MainActor
|
||||
final class RulesWorkflowPublicationTests: XCTestCase {
|
||||
func testNativeControlsWritingTheSameQueryDoNotRepublishThePage() async {
|
||||
let workflow = RulesWorkflow(loadCustom: { [] }, loadBuiltin: { rulesFixture($0) })
|
||||
let workflow = RulesWorkflow(
|
||||
loadCustom: { [] }, builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
|
||||
await workflow.refresh()
|
||||
workflow.query(RulesQuery(source: .gfwlist))
|
||||
var changes = 0
|
||||
@@ -16,7 +17,8 @@ final class RulesWorkflowPublicationTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testNativeControlsWritingTheSameVisibleSelectionDoNotRepublishThePage() async {
|
||||
let workflow = RulesWorkflow(loadCustom: { [] }, loadBuiltin: { rulesFixture($0) })
|
||||
let workflow = RulesWorkflow(
|
||||
loadCustom: { [] }, builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
|
||||
await workflow.refresh()
|
||||
workflow.select([.noDotHostname])
|
||||
var changes = 0
|
||||
|
||||
@@ -27,14 +27,14 @@ final class RulesWorkflowTests: XCTestCase {
|
||||
outcome: .persistenceFailed, document: try? store.loadDocument())
|
||||
}
|
||||
},
|
||||
loadBuiltin: { source in
|
||||
builtinSnapshots: BuiltinRuleSnapshots(loader: { source in
|
||||
guard source == .gfwlist else { return rulesFixture(source) }
|
||||
return rulesFixture(
|
||||
source,
|
||||
rules: [
|
||||
ProxyRule(action: .proxy, match: broad.match)
|
||||
])
|
||||
})
|
||||
}))
|
||||
await workflow.refresh()
|
||||
let version = workflow.snapshot.version
|
||||
let covered = try XCTUnwrap(workflow.snapshot.rows.first { $0.identity == narrow.identity })
|
||||
@@ -72,7 +72,9 @@ final class RulesWorkflowTests: XCTestCase {
|
||||
rules: [ProxyRule(action: .direct, match: match)])
|
||||
let workflow = RulesWorkflow(
|
||||
loadCustom: { [custom] },
|
||||
loadBuiltin: { source in source == .geolocationCN ? snapshot : rulesFixture(source) })
|
||||
builtinSnapshots: BuiltinRuleSnapshots(loader: { source in
|
||||
source == .geolocationCN ? snapshot : rulesFixture(source)
|
||||
}))
|
||||
await workflow.refresh()
|
||||
workflow.query(RulesQuery(search: "EXAMPLE", action: .direct, source: .geolocationCN))
|
||||
let row = try XCTUnwrap(workflow.snapshot.rows.first)
|
||||
@@ -85,9 +87,9 @@ final class RulesWorkflowTests: XCTestCase {
|
||||
func testWrongBuiltinSourceIsAnIncompleteCollectionRatherThanMislabeledRules() async {
|
||||
let workflow = RulesWorkflow(
|
||||
loadCustom: { [] },
|
||||
loadBuiltin: { _ in
|
||||
builtinSnapshots: BuiltinRuleSnapshots(loader: { _ in
|
||||
rulesFixture(.geolocationCN)
|
||||
})
|
||||
}))
|
||||
await workflow.refresh()
|
||||
XCTAssertFalse(workflow.snapshot.isComplete)
|
||||
XCTAssertEqual(workflow.snapshot.issues.count, 2)
|
||||
@@ -97,7 +99,8 @@ final class RulesWorkflowTests: XCTestCase {
|
||||
let direct = CustomRule(action: .direct, match: try RuleMatch(domainSuffix: "example.com"))
|
||||
let proxy = CustomRule(action: .proxy, match: try RuleMatch(domainExact: "outside.net"))
|
||||
let workflow = RulesWorkflow(
|
||||
loadCustom: { [direct, proxy] }, loadBuiltin: { rulesFixture($0) })
|
||||
loadCustom: { [direct, proxy] },
|
||||
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
|
||||
await workflow.refresh()
|
||||
workflow.select([.rule(direct.identity), .rule(proxy.identity), .noDotHostname])
|
||||
workflow.query(RulesQuery(search: "EXAMPLE", action: .direct, source: .custom))
|
||||
@@ -114,7 +117,8 @@ final class RulesWorkflowTests: XCTestCase {
|
||||
CustomRule(action: .proxy, match: try RuleMatch(domainExact: "z.net")),
|
||||
CustomRule(action: .direct, match: try RuleMatch(domainExact: "a.net")),
|
||||
]
|
||||
let workflow = RulesWorkflow(loadCustom: { entries }, loadBuiltin: { rulesFixture($0) })
|
||||
let workflow = RulesWorkflow(
|
||||
loadCustom: { entries }, builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
|
||||
await workflow.refresh()
|
||||
let version = workflow.snapshot.version
|
||||
workflow.query(RulesQuery(source: .custom))
|
||||
@@ -134,10 +138,10 @@ final class RulesWorkflowTests: XCTestCase {
|
||||
func testCorruptUserDocumentAndMissingBuiltinStayDistinctFromEmptySearch() async {
|
||||
let workflow = RulesWorkflow(
|
||||
loadCustom: { throw CustomRuleStoreError.corrupt(detail: "fixture") },
|
||||
loadBuiltin: { source in
|
||||
builtinSnapshots: BuiltinRuleSnapshots(loader: { source in
|
||||
if source == .chinaIPv4 { throw RuleSnapshotError.missing }
|
||||
return rulesFixture(source)
|
||||
})
|
||||
}))
|
||||
await workflow.refresh()
|
||||
XCTAssertFalse(workflow.snapshot.isComplete)
|
||||
XCTAssertEqual(
|
||||
@@ -154,14 +158,7 @@ final class RulesWorkflowTests: XCTestCase {
|
||||
let bundle = AppArtifact.bundle
|
||||
let workflow = RulesWorkflow(
|
||||
loadCustom: { [] },
|
||||
loadBuiltin: { source in
|
||||
switch source {
|
||||
case .geolocationCN: try BuiltinRuleCatalog.loadGeolocationCN(from: bundle)
|
||||
case .chinaIPv4: try BuiltinRuleCatalog.loadChinaIPv4(from: bundle)
|
||||
case .gfwlist: try BuiltinRuleCatalog.loadGFWList(from: bundle)
|
||||
case .custom, .fixed: throw RuleSnapshotError.missing
|
||||
}
|
||||
})
|
||||
builtinSnapshots: BuiltinRuleSnapshots(bundle: bundle))
|
||||
await workflow.refresh()
|
||||
XCTAssertTrue(workflow.snapshot.isComplete, "\(workflow.snapshot.issues)")
|
||||
XCTAssertEqual(workflow.snapshot.sources.count, 5)
|
||||
@@ -185,7 +182,8 @@ final class RulesWorkflowTests: XCTestCase {
|
||||
let new = CustomRule(action: .proxy, match: try RuleMatch(domainExact: "new.net"))
|
||||
try store.save([old])
|
||||
let workflow = RulesWorkflow(
|
||||
loadCustom: { try store.load() }, loadBuiltin: { rulesFixture($0) })
|
||||
loadCustom: { try store.load() },
|
||||
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
|
||||
await workflow.refresh()
|
||||
workflow.query(RulesQuery(source: .custom))
|
||||
workflow.select([.rule(old.identity)])
|
||||
@@ -215,7 +213,8 @@ final class RulesWorkflowTests: XCTestCase {
|
||||
let firstRead = XCTestExpectation(description: "first read")
|
||||
let barrier = RulesReadBarrier(firstRead: firstRead)
|
||||
let workflow = RulesWorkflow(
|
||||
loadCustom: { try barrier.read() }, loadBuiltin: { rulesFixture($0) })
|
||||
loadCustom: { try barrier.read() },
|
||||
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
|
||||
let older = Task { await workflow.refresh() }
|
||||
await fulfillment(of: [firstRead], timeout: 3)
|
||||
defer { barrier.release.signal() }
|
||||
@@ -237,8 +236,12 @@ extension RulesWorkflowTests {
|
||||
func testCandidatePermutationKeepsVersionAndCoverageWhilePreservingInputOrder() async throws {
|
||||
let broad = CustomRule(action: .direct, match: try RuleMatch(domainSuffix: "example.com"))
|
||||
let narrow = CustomRule(action: .proxy, match: try RuleMatch(domainExact: "www.example.com"))
|
||||
let forward = RulesWorkflow(loadCustom: { [broad, narrow] }, loadBuiltin: { rulesFixture($0) })
|
||||
let reverse = RulesWorkflow(loadCustom: { [narrow, broad] }, loadBuiltin: { rulesFixture($0) })
|
||||
let forward = RulesWorkflow(
|
||||
loadCustom: { [broad, narrow] },
|
||||
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
|
||||
let reverse = RulesWorkflow(
|
||||
loadCustom: { [narrow, broad] },
|
||||
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
|
||||
await forward.refresh()
|
||||
await reverse.refresh()
|
||||
XCTAssertEqual(forward.snapshot.version, reverse.snapshot.version)
|
||||
@@ -255,7 +258,8 @@ extension RulesWorkflowTests {
|
||||
let exact = CustomRule(action: .direct, match: try RuleMatch(domainExact: "example.com"))
|
||||
let suffix = CustomRule(action: .direct, match: try RuleMatch(domainSuffix: "example.com"))
|
||||
let workflow = RulesWorkflow(
|
||||
loadCustom: { [suffix, exact] }, loadBuiltin: { rulesFixture($0) })
|
||||
loadCustom: { [suffix, exact] },
|
||||
builtinSnapshots: BuiltinRuleSnapshots(loader: { rulesFixture($0) }))
|
||||
await workflow.refresh()
|
||||
let version = workflow.snapshot.version
|
||||
let identities = workflow.snapshot.rows.map(\.identity)
|
||||
@@ -292,12 +296,8 @@ extension RulesWorkflowTests {
|
||||
func rulesFixture(
|
||||
_ source: RulesSource, rules: [ProxyRule] = []
|
||||
) -> RuleSnapshot {
|
||||
let kind: RuleSourceKind
|
||||
switch source {
|
||||
case .geolocationCN: kind = .geolocationCN
|
||||
case .chinaIPv4: kind = .chinaIPv4
|
||||
case .gfwlist: kind = .gfwlist
|
||||
case .custom, .fixed: kind = .custom
|
||||
guard let kind = source.sourceKind else {
|
||||
preconditionFailure("Fixed policy has no rule snapshot")
|
||||
}
|
||||
return RuleSnapshot(
|
||||
metadata: RuleSnapshotMetadata(
|
||||
|
||||
Reference in New Issue
Block a user