mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-08 11:14:51 +00:00
Six sites re-asserted the predicate waitUntilRuntimeSettles already checks, with no suspension point in between, so they could never fail; their intent messages are folded into comments where one was not already stated. Two commit-failure cases asserted the fake's bookkeeping (updateCalls stays empty) which the fake guarantees by throwing before recording; the outcome and committed-snapshot assertions already prove the real behavior. Also drop a no-op loop that mutated a discarded struct copy, a duplicated isWellFormed line, two WorkspaceRoute cases subsumed by the constructor and the publish-count case, a duplicated presentation-message assertion, and the round-trip halves of two payload-shape cases already covered case-by-case. Co-Authored-By: DeepSeek Harness <noreply@deepseek.com>
179 lines
7.5 KiB
Swift
179 lines
7.5 KiB
Swift
import XCTest
|
|
|
|
@testable import ShadowsocksX_NG2
|
|
|
|
/// 订阅快照的目录应用语义:固定分组身份保留;远端删除不留
|
|
/// 墓碑;结构、字段与顺序远端权威,不存在本地启用覆盖层。
|
|
final class SubscriptionSnapshotApplicationTests: XCTestCase {
|
|
private let groupID = NodeID(rawValue: "sub1:fixed-group")
|
|
|
|
private func makeCatalog() throws -> ConfigurationCatalog {
|
|
var catalog = ConfigurationCatalog()
|
|
try catalog.addGroup("订阅分组", source: .subscription, id: groupID)
|
|
let nested = NodeID(rawValue: "sub1:g:jp")
|
|
try catalog.addGroup("日本", source: .subscription, id: nested, to: groupID)
|
|
let serverA = NodeID(rawValue: "sub1:id:a")
|
|
try catalog.addServer(
|
|
CatalogFixtures.serverFields(remark: "香港 01"), source: .subscription, id: serverA,
|
|
to: groupID)
|
|
let serverB = NodeID(rawValue: "sub1:id:b")
|
|
try catalog.addServer(
|
|
CatalogFixtures.serverFields(remark: "日本 02"), source: .subscription, id: serverB,
|
|
to: nested)
|
|
return catalog
|
|
}
|
|
|
|
private func leaf(_ id: String, fields: ServerFields = CatalogFixtures.serverFields(remark: ""))
|
|
-> CatalogSubscriptionSnapshot.Child
|
|
{
|
|
.server(CatalogSubscriptionSnapshot.ServerLeaf(id: NodeID(rawValue: id), fields: fields))
|
|
}
|
|
|
|
private func group(_ id: String, _ name: String, children: [CatalogSubscriptionSnapshot.Child])
|
|
-> CatalogSubscriptionSnapshot.Child
|
|
{
|
|
.group(
|
|
CatalogSubscriptionSnapshot.Group(id: NodeID(rawValue: id), name: name, children: children))
|
|
}
|
|
|
|
// MARK: 快照应用
|
|
|
|
func testSnapshotReplacesSubtreeWithoutLocalEnableOverlay() throws {
|
|
var catalog = try makeCatalog()
|
|
|
|
let document = CatalogSubscriptionSnapshot(
|
|
name: "远端新名",
|
|
root: .init(
|
|
id: groupID, name: "ignored-root",
|
|
children: [
|
|
leaf("sub1:id:b"),
|
|
group(
|
|
"sub1:g:jp", "JP", children: [leaf("sub1:id:a"), leaf("sub1:id:new")]),
|
|
]))
|
|
let removed = try catalog.applySubscriptionSnapshot(document, into: groupID)
|
|
|
|
XCTAssertEqual(removed.count, 2, "旧树两台服务器全部视为移除(b 从嵌套组移到根)")
|
|
// 固定分组身份保持不变,名称跟随远端;远端子序原样落目录。
|
|
let fixed = try XCTUnwrap(catalog.entry(for: groupID))
|
|
XCTAssertEqual(
|
|
try catalog.children(of: groupID),
|
|
[NodeID(rawValue: "sub1:id:b"), NodeID(rawValue: "sub1:g:jp")])
|
|
guard case .group(let fields) = fixed.kind else { return XCTFail("固定分组应存在") }
|
|
XCTAssertEqual(fields.name, "远端新名")
|
|
XCTAssertEqual(fixed.source, .subscription)
|
|
// 旧树已重建,无墓碑:固定分组 + 根序 b + 嵌套组 + a + 新节点。
|
|
XCTAssertEqual(catalog.entries.count, 5)
|
|
}
|
|
|
|
func testRemovedRemoteMembersLeaveNoTombstone() throws {
|
|
var catalog = try makeCatalog()
|
|
let removedTarget = NodeID(rawValue: "sub1:id:b")
|
|
|
|
let document = CatalogSubscriptionSnapshot(
|
|
name: "订阅分组", root: .init(id: groupID, name: "", children: [leaf("sub1:id:a")]))
|
|
_ = try catalog.applySubscriptionSnapshot(document, into: groupID)
|
|
|
|
XCTAssertNil(catalog.entry(for: removedTarget), "远端删除即移除")
|
|
XCTAssertNil(catalog.entry(for: NodeID(rawValue: "sub1:g:jp")), "空嵌套组一并移除")
|
|
XCTAssertEqual(catalog.entries.count, 2)
|
|
}
|
|
|
|
func testFixedGroupStateIsFullyRemoteOwnedOnRefresh() throws {
|
|
var catalog = try makeCatalog()
|
|
|
|
let document = CatalogSubscriptionSnapshot(
|
|
name: "新名", root: .init(id: groupID, name: "", children: [leaf("sub1:id:a")]))
|
|
_ = try catalog.applySubscriptionSnapshot(document, into: groupID)
|
|
|
|
XCTAssertEqual(try XCTUnwrap(catalog.entry(for: groupID)).displayName, "新名")
|
|
}
|
|
|
|
func testApplyToManualGroupRejected() throws {
|
|
var catalog = ConfigurationCatalog()
|
|
let manual = try catalog.addGroup("手动组")
|
|
let document = CatalogSubscriptionSnapshot(
|
|
name: "x", root: .init(id: groupID, name: "", children: []))
|
|
|
|
XCTAssertThrowsError(try catalog.applySubscriptionSnapshot(document, into: manual))
|
|
}
|
|
|
|
func testApplyToMissingGroupRejected() throws {
|
|
var catalog = ConfigurationCatalog()
|
|
let document = CatalogSubscriptionSnapshot(
|
|
name: "x", root: .init(id: groupID, name: "", children: []))
|
|
|
|
XCTAssertThrowsError(
|
|
try catalog.applySubscriptionSnapshot(document, into: NodeID(rawValue: "ghost")))
|
|
}
|
|
|
|
// MARK: 订阅子树删除
|
|
|
|
func testRemoveSubscriptionSubtreeRemovesEverythingAndReportsCredentials() throws {
|
|
var catalog = try makeCatalog()
|
|
let manual = try catalog.addGroup("手动组", id: NodeID(rawValue: "manual:g"))
|
|
_ = try catalog.addTestServer("手动服务器", id: NodeID(rawValue: "manual:s"), to: manual)
|
|
|
|
let removed = try catalog.removeSubscriptionSubtree(of: groupID)
|
|
|
|
XCTAssertEqual(removed.count, 4, "固定分组 + 嵌套组 + 两台服务器")
|
|
XCTAssertEqual(catalog.entries.count, 2, "手动子树不受影响")
|
|
XCTAssertFalse(catalog.rootChildren.contains(groupID))
|
|
XCTAssertTrue(removed.allSatisfy { $0.source == .subscription })
|
|
// 返回条目带凭据引用供调用方清理。
|
|
let credentialRefs = removed.compactMap { entry -> String? in
|
|
if case .server(let fields) = entry.kind { return fields.password }
|
|
return nil
|
|
}
|
|
XCTAssertEqual(credentialRefs.count, 2)
|
|
}
|
|
|
|
func testRemoveMissingSubscriptionRejected() {
|
|
var catalog = ConfigurationCatalog()
|
|
XCTAssertThrowsError(
|
|
try catalog.removeSubscriptionSubtree(of: NodeID(rawValue: "ghost")))
|
|
}
|
|
|
|
// MARK: 订阅子树变更后重展开(接线语义: 验收 4)
|
|
|
|
func testActiveTargetInsideRefreshedSubtreeStillExpandsAfterSnapshot() throws {
|
|
var catalog = try makeCatalog()
|
|
var machine = ActivationStateMachine(activeTargetID: NodeID(rawValue: "sub1:id:a"))
|
|
|
|
// 快照后 a 仍在(身份不变)→ 目标有效,重展开产出部署效应。叶子无插件,
|
|
// 激活校验只依赖凭据解析;快照重建后按目录里的真实引用解析秘密。
|
|
let document = CatalogSubscriptionSnapshot(
|
|
name: "订阅分组",
|
|
root: .init(
|
|
id: groupID, name: "",
|
|
children: [
|
|
.server(
|
|
CatalogSubscriptionSnapshot.ServerLeaf(
|
|
id: NodeID(rawValue: "sub1:id:a"),
|
|
fields: ServerFields(
|
|
address: "203.0.113.1", port: 8388,
|
|
encryptionMethod: "aes-256-gcm", password: "password")))
|
|
]))
|
|
_ = try catalog.applySubscriptionSnapshot(document, into: groupID)
|
|
|
|
let effect = machine.catalogDidCommit(
|
|
catalog, plugins: NoManagedPluginProvider(),
|
|
options: RuntimeDocumentOptions(listen: ProxyListeningConfiguration()))
|
|
guard case .deployed(let configuration) = try XCTUnwrap(effect) else {
|
|
return XCTFail("身份未变的活动目标应部署而非清除")
|
|
}
|
|
XCTAssertEqual(configuration.targetID, NodeID(rawValue: "sub1:id:a"))
|
|
|
|
// 快照清空该目标 → 清除并停止。
|
|
let empty = CatalogSubscriptionSnapshot(
|
|
name: "订阅分组", root: .init(id: groupID, name: "", children: []))
|
|
_ = try catalog.applySubscriptionSnapshot(empty, into: groupID)
|
|
let cleared = machine.catalogDidCommit(
|
|
catalog, plugins: NoManagedPluginProvider(),
|
|
options: RuntimeDocumentOptions(listen: ProxyListeningConfiguration()))
|
|
guard case .clearedAndStopped = try XCTUnwrap(cleared) else {
|
|
return XCTFail("目标随远端删除消失后应清除并停止")
|
|
}
|
|
XCTAssertNil(machine.activeTargetID)
|
|
}
|
|
}
|