Files
ShadowsocksX-NG/ShadowsocksX-NG2/Tests/ActivationStateFileStoreTests.swift
T

105 lines
4.1 KiB
Swift

import XCTest
@testable import ShadowsocksX_NG2
/// 活动目标持久化缝(票 #26):往返无损、缺失/损坏按安全侧恢复、权限基线。
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(), "清除即整文件替换,不再残留旧目标")
}
// MARK: 权限基线
func testSaveCreatesDirectory0700AndFile0600() throws {
try store.save(activeTargetID: NodeID(rawValue: "s1"))
let fileAttributes = try FileManager.default.attributesOfItem(atPath: store.fileURL.path)
XCTAssertEqual(fileAttributes[.posixPermissions] as? Int, 0o600, "文件权限 0600")
let dirAttributes = try FileManager.default.attributesOfItem(atPath: workDir.path)
XCTAssertEqual(dirAttributes[.posixPermissions] as? Int, 0o700, "目录权限 0700")
}
func testDefaultFileURLMatchesRuntimeDirectoryContract() {
XCTAssertTrue(
ActivationStateFileStore.defaultFileURL().path.hasSuffix("ShadowsocksX-NG2/activation.json")
)
}
// MARK: 与状态机的生命周期往返
func testActivationLifecycleRoundTripThroughFileStore() throws {
var catalog = ConfigurationCatalog()
let credentials = InMemoryCredentialStore()
let leaf = try ActivationFixture.addPlainServer("a", in: &catalog, credentials: credentials)
var machine = ActivationStateMachine()
_ = try ActivationFixture.activate(&machine, leaf, in: catalog, credentials: credentials)
try store.save(activeTargetID: machine.activeTargetID)
// 下次启动:从文件恢复目标,载入目录后立即重校验(D5「重新校验同步」)。
var restored = ActivationStateMachine(activeTargetID: try store.loadActiveTargetID())
let configuration = try ActivationFixture.requireDeployed(
ActivationFixture.commit(&restored, catalog, credentials: credentials))
XCTAssertEqual(configuration.targetID, leaf, "恢复的目标重放派生")
// 目标随后失效:清除并停止,持久化同步清空。
try catalog.remove(leaf)
let failure = try ActivationFixture.requireCleared(
ActivationFixture.commit(&restored, catalog, credentials: credentials))
XCTAssertEqual(failure, .targetNotFound(leaf))
try store.save(activeTargetID: restored.activeTargetID)
XCTAssertNil(try store.loadActiveTargetID(), "清除随持久化落盘,重启不再复活旧目标")
}
}