mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-08 11:24:50 +00:00
Every case here restates a rule another test already pins with the same or a wider input class: - the activation group case is byte-identical to its neighbour, minus an extra assertion; the rule snapshot version guard is re-driven with a second value of the same schemaVersion check; the global-mode deployment-failure case is a mode-literal copy of the strictly stronger direct-mode one; the clear- failure case duplicates the deferred-retry sibling - two RuntimeDocument projection cases only re-map locals, already asserted as full array equality; the IPv6-LAN endpoint case repeats the direct-mode case (the derivation reads only ports) - the three HTTP export cases varied only listenerMode while the derivation ignores it and one name contradicted its own expectation; one loop over all listener modes now states the real rule - the 0700/0600 permission baseline is one AtomicFileWriter behavior and stays pinned through RuntimeFileStoreTests, which also covers self-heal and atomic replace The command-boundary case keeps its mixed-scenario isolation check (four commands, four independent adapter counters) and drops only its unfalsifiable queriedTargetIDs claim. Co-Authored-By: DeepSeek Harness <noreply@deepseek.com>
94 lines
3.5 KiB
Swift
94 lines
3.5 KiB
Swift
import XCTest
|
|
|
|
@testable import ShadowsocksX_NG2
|
|
|
|
/// 活动目标持久化缝:往返无损、缺失/损坏按安全侧恢复、权限基线。
|
|
final class ActivationStateFileStoreTests: XCTestCase {
|
|
private var workDir: URL!
|
|
private var store: ActivationStateFileStore!
|
|
|
|
override func setUpWithError() throws {
|
|
try super.setUpWithError()
|
|
workDir = FileManager.default.temporaryDirectory
|
|
.appendingPathComponent("activation-file-store-tests-\(UUID().uuidString)", isDirectory: true)
|
|
try FileManager.default.createDirectory(at: workDir, withIntermediateDirectories: true)
|
|
store = ActivationStateFileStore(fileURL: workDir.appendingPathComponent("activation.json"))
|
|
}
|
|
|
|
override func tearDownWithError() throws {
|
|
try FileManager.default.removeItem(at: workDir)
|
|
try super.tearDownWithError()
|
|
}
|
|
|
|
// MARK: 缺失与损坏
|
|
|
|
func testMissingFileLoadsAsNoActiveTarget() throws {
|
|
XCTAssertNil(try store.loadActiveTargetID(), "文件缺失按从未激活处理")
|
|
}
|
|
|
|
func testBrokenJSONLoadsAsNoActiveTargetOnTheSafeSide() throws {
|
|
try "{ not json {{{".write(to: store.fileURL, atomically: true, encoding: .utf8)
|
|
|
|
XCTAssertNil(
|
|
try store.loadActiveTargetID(), "损坏按安全侧恢复:无活动目标,代理保持停止")
|
|
}
|
|
|
|
func testUnknownSchemaVersionLoadsAsNoActiveTarget() throws {
|
|
let payload = """
|
|
{"version": 99, "activeTargetID": "s1"}
|
|
"""
|
|
try payload.write(to: store.fileURL, atomically: true, encoding: .utf8)
|
|
|
|
XCTAssertNil(try store.loadActiveTargetID())
|
|
}
|
|
|
|
// MARK: 往返无损
|
|
|
|
func testRoundTripPreservesTargetIdentity() throws {
|
|
let target = NodeID(rawValue: "manual:group-1")
|
|
|
|
try store.save(activeTargetID: target)
|
|
|
|
XCTAssertEqual(try store.loadActiveTargetID(), target, "目标身份跨持久化无损")
|
|
}
|
|
|
|
func testSavingNilClearsPersistedTarget() throws {
|
|
try store.save(activeTargetID: NodeID(rawValue: "s1"))
|
|
|
|
try store.save(activeTargetID: nil)
|
|
|
|
XCTAssertNil(try store.loadActiveTargetID(), "清除即整文件替换,不再残留旧目标")
|
|
}
|
|
|
|
func testDefaultFileURLMatchesRuntimeDirectoryContract() {
|
|
XCTAssertTrue(
|
|
ActivationStateFileStore.defaultFileURL().path.hasSuffix(
|
|
"ShadowsocksX-NG2/configuration/activation.json")
|
|
)
|
|
}
|
|
|
|
// MARK: 与状态机的生命周期往返
|
|
|
|
func testActivationLifecycleRoundTripThroughFileStore() throws {
|
|
var catalog = ConfigurationCatalog()
|
|
let leaf = try ActivationFixture.addPlainServer("a", in: &catalog)
|
|
var machine = ActivationStateMachine()
|
|
_ = try ActivationFixture.activate(&machine, leaf, in: catalog)
|
|
try store.save(activeTargetID: machine.activeTargetID)
|
|
|
|
// 下次启动:从文件恢复目标,载入目录后立即重校验(「重新校验同步」)。
|
|
var restored = ActivationStateMachine(activeTargetID: try store.loadActiveTargetID())
|
|
let configuration = try ActivationFixture.requireDeployed(
|
|
ActivationFixture.commit(&restored, catalog))
|
|
XCTAssertEqual(configuration.targetID, leaf, "恢复的目标重放派生")
|
|
|
|
// 目标随后失效:清除并停止,持久化同步清空。
|
|
try catalog.remove(leaf)
|
|
let failure = try ActivationFixture.requireCleared(
|
|
ActivationFixture.commit(&restored, catalog))
|
|
XCTAssertEqual(failure, .targetNotFound(leaf))
|
|
try store.save(activeTargetID: restored.activeTargetID)
|
|
XCTAssertNil(try store.loadActiveTargetID(), "清除随持久化落盘,重启不再复活旧目标")
|
|
}
|
|
}
|