Files

177 lines
7.4 KiB
Swift

import Foundation
import Testing
@testable import ShadowsocksX_NG2
@MainActor
struct RuleDocumentSessionTests {
@Test(arguments: RulePreset.allCases)
func reverseActionCommitIsRejectedWithoutChangingSavedFacts(_ preset: RulePreset) throws {
let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
defer { try? FileManager.default.removeItem(at: directory) }
let store = CustomRuleStore(
preset: preset, baseURL: directory.appendingPathComponent("rules.json"))
let session = RuleDocumentSession(store: store)
let original = CustomRuleDocument(rules: [
CustomRule(
action: preset == .chinaDirect ? .direct : .proxy,
match: try RuleMatch(domainExact: "kept.example"))
])
#expect(session.commit(original).outcome == .saved)
let bytes = try Data(contentsOf: store.fileURL)
let invalid = CustomRuleDocument(rules: [
CustomRule(
action: preset == .chinaDirect ? .proxy : .direct,
match: try RuleMatch(domainExact: "reverse.example"))
])
let result = session.commit(invalid)
#expect(
result.outcome == .invalidDocument(detail: "Custom rule action must match its rule scheme"))
#expect(result.document == original)
#expect(session.revision == 1)
#expect(try Data(contentsOf: store.fileURL) == bytes)
#expect(throws: CustomRuleStoreError.self) { try store.saveDocument(invalid) }
#expect(try Data(contentsOf: store.fileURL) == bytes)
}
@Test func successfulCommitPublishesSavedFactsAndOnlyChangedContentAdvancesRevision() throws {
let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
defer { try? FileManager.default.removeItem(at: directory) }
let store = CustomRuleStore(
preset: .chinaProxy, baseURL: directory.appendingPathComponent("rules.json"))
let session = RuleDocumentSession(store: store)
let document = CustomRuleDocument(rules: [
CustomRule(action: .proxy, match: try RuleMatch(domainExact: "saved.example"))
])
let result = session.commit(document)
#expect(result.outcome == .saved)
#expect(result.document == document)
#expect(try session.load() == document)
#expect(session.revision == 1)
#expect(session.commit(document).outcome == .saved)
#expect(session.revision == 1)
#expect(try RuleDocumentSession(store: store).load() == document)
}
}
extension RuleDocumentSessionTests {
@Test(arguments: ["fixedConflict", "duplicate", "fixedDisabled"])
func invalidCommitRetainsSavedFactsAndRevision(reason: String) 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 session = RuleDocumentSession(store: store)
let kept = CustomRule(action: .direct, match: try RuleMatch(domainExact: "kept.example"))
let original = CustomRuleDocument(rules: [kept])
#expect(session.commit(original).outcome == .saved)
let proposal: CustomRuleDocument
switch reason {
case "fixedConflict":
proposal = CustomRuleDocument(rules: [
kept,
CustomRule(action: .proxy, match: try RuleMatch(ipv4CIDR: "127.0.0.0/8")),
])
case "duplicate":
proposal = CustomRuleDocument(rules: [
kept,
CustomRule(action: kept.action, match: kept.rule.match),
])
default:
proposal = CustomRuleDocument(
rules: [kept],
disabledIdentities: [
ProxyRule(action: .direct, match: try RuleMatch(ipv4CIDR: "127.0.0.0/8"))
])
}
let result = session.commit(proposal)
if reason == "fixedDisabled" {
#expect(result.outcome == .invalidDocument(detail: "Fixed local policy cannot be disabled"))
} else {
guard case .rejected(let rejected) = result.outcome else {
Issue.record("Expected whole-document rejection, got \(result.outcome)")
return
}
#expect(
rejected.map(\.reason).allSatisfy {
$0 == (reason == "duplicate" ? .duplicate : .conflictsWithFixedLocalScope)
})
#expect(!rejected.isEmpty)
}
#expect(result.document == original)
#expect(session.current == original)
#expect(session.revision == 1)
#expect(try RuleDocumentSession(store: store).load() == original)
}
}
extension RuleDocumentSessionTests {
@Test func corruptFirstLoadRejectsCommitWithoutOverwritingFile() throws {
let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
defer { try? FileManager.default.removeItem(at: directory) }
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
let store = CustomRuleStore(fileURL: directory.appendingPathComponent("rules.json"))
let corrupt = Data("broken".utf8)
try corrupt.write(to: store.fileURL)
let session = RuleDocumentSession(store: store)
let result = session.commit(CustomRuleDocument(rules: []))
#expect(result.outcome == .persistenceFailed)
#expect(result.document == nil)
#expect(session.current == nil)
#expect(session.revision == 0)
#expect(try Data(contentsOf: store.fileURL) == corrupt)
}
@Test func failedWriteRetainsOwnedFactsAndRevision() throws {
let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
defer { try? FileManager.default.removeItem(at: directory) }
let storage = directory.appendingPathComponent("storage")
let store = CustomRuleStore(fileURL: storage.appendingPathComponent("rules.json"))
let session = RuleDocumentSession(store: store)
let original = CustomRuleDocument(rules: [
CustomRule(action: .direct, match: try RuleMatch(domainExact: "kept.example"))
])
#expect(session.commit(original).outcome == .saved)
let backup = directory.appendingPathComponent("backup")
try FileManager.default.moveItem(at: storage, to: backup)
try Data("blocked".utf8).write(to: storage)
let result = session.commit(CustomRuleDocument(rules: []))
#expect(result.outcome == .persistenceFailed)
#expect(result.document == original)
#expect(try session.load() == original)
#expect(session.revision == 1)
try FileManager.default.removeItem(at: storage)
try FileManager.default.moveItem(at: backup, to: storage)
#expect(try RuleDocumentSession(store: store).load() == original)
}
@Test func overlappingOppositeActionsAndOrphanedDisablementRemainSavedIntent() 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 session = RuleDocumentSession(store: store)
let document = CustomRuleDocument(
rules: [
CustomRule(action: .direct, match: try RuleMatch(domainSuffix: "example.com")),
CustomRule(action: .proxy, match: try RuleMatch(domainExact: "a.example.com")),
CustomRule(action: .direct, match: try RuleMatch(domainExact: "a.example.com")),
],
disabledIdentities: [
ProxyRule(action: .proxy, match: try RuleMatch(domainExact: "orphan.example"))
])
let result = session.commit(document)
#expect(result.outcome == .saved)
#expect(result.document == document)
#expect(try RuleDocumentSession(store: store).load() == document)
}
}