Files
Qiu YuzhouandDeepSeek Harness e7116a5ebd test: merge duplicated cases into the stronger one
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>
2026-10-21 01:46:29 +08:00

193 lines
7.1 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import XCTest
@testable import ShadowsocksX_NG2
/// 持久化缝:往返无损、文件缺失/损坏行为明确、权限基线、不含明文秘密。
final class CatalogFileStoreTests: XCTestCase {
private var workDir: URL!
var store: CatalogFileStore!
override func setUpWithError() throws {
try super.setUpWithError()
workDir = FileManager.default.temporaryDirectory
.appendingPathComponent("catalog-file-store-tests-\(UUID().uuidString)", isDirectory: true)
try FileManager.default.createDirectory(at: workDir, withIntermediateDirectories: true)
store = makeTestCatalogStore(fileURL: workDir.appendingPathComponent("catalog.json"))
}
override func tearDownWithError() throws {
try FileManager.default.removeItem(at: workDir)
try super.tearDownWithError()
}
// MARK: 往返无损
func testRoundTripIsLosslessAndKeepsIdentity() throws {
let original = try Self.makeRichCatalog()
try store.save(CatalogDocument(catalog: original))
let loaded = try store.load()
XCTAssertEqual(loaded.catalog, original, "结构、顺序、字段与订阅夹具全部无损")
let serverID = NodeID(rawValue: "manual:server")
let server = try XCTUnwrap(loaded.catalog.entry(for: serverID))
XCTAssertEqual(server.id, serverID, "身份跨持久化稳定")
guard case .server(let fields) = server.kind else { return XCTFail("目标应是服务器叶子") }
XCTAssertEqual(fields.pluginProgram, "v2ray-plugin", "插件程序引用原样保留")
XCTAssertEqual(fields.pluginOptions, "options")
}
func testSecondSaveReplacesContent() throws {
var catalog = ConfigurationCatalog()
let first = try catalog.addTestServer("first")
try store.save(CatalogDocument(catalog: catalog))
try catalog.remove(first)
_ = try catalog.addTestServer("second")
try store.save(CatalogDocument(catalog: catalog))
XCTAssertEqual(try store.load().catalog, catalog)
XCTAssertFalse(try store.load().catalog.contains(first), "旧内容被整体替换而非追加")
}
// MARK: 敏感信息
func testPersistedFileContainsNoPlaintextSecrets() throws {
var catalog = ConfigurationCatalog()
let fields = ServerFields(
address: "203.0.113.7",
port: 8388,
encryptionMethod: "aes-256-gcm",
password: "TOPSECRET-密码",
remark: "含插件",
pluginProgram: "v2ray-plugin",
pluginOptions: "obfs-local;obfs=http"
)
_ = try catalog.addServer(fields)
try store.save(CatalogDocument(catalog: catalog))
let raw = try Data(contentsOf: store.fileURL)
XCTAssertFalse(raw.range(of: Data("TOPSECRET-密码".utf8)) != nil, "密码明文不得进入持久化文件")
XCTAssertFalse(raw.range(of: Data("obfs-local;obfs=http".utf8)) != nil, "插件参数明文不得进入持久化文件")
}
/// 富夹具:订阅子树 + 手动组嵌套 + 空组 + 无效节点 + 重排后子序。
static func makeRichCatalog() throws -> ConfigurationCatalog {
var catalog = try CatalogFixtures.makeSubscriptionFixture(prefix: "sub9").catalog
let group = NodeID(rawValue: "manual:group")
try catalog.addGroup("手动组", id: group)
let server = NodeID(rawValue: "manual:server")
try catalog.addServer(
ServerFields(
address: "198.51.100.5",
port: 443,
encryptionMethod: "chacha20-ietf-poly1305",
password: "password",
remark: "带插件",
pluginProgram: "v2ray-plugin",
pluginOptions: "options"
),
id: server,
to: group
)
_ = try catalog.addTestServer("无效", to: group)
try catalog.addGroup("空组", id: NodeID(rawValue: "manual:empty"))
try catalog.move(server, to: group, index: 0)
return catalog
}
// MARK: 订阅记录文档(v2)
func testSubscriptionRecordRoundTripsWithStatus() throws {
let catalog = try CatalogFixtures.makeSubscriptionFixture(prefix: "sub2").catalog
let record = SubscriptionRecord(
id: NodeID(rawValue: "sub2:source"),
groupID: NodeID(rawValue: "sub2:group"),
url: "https://provider.example/sub",
status: .failed(
at: Date(timeIntervalSince1970: 1_789_000_000),
failure: .httpStatus(code: 503)))
try store.save(CatalogDocument(catalog: catalog, subscriptions: [record]))
let loaded = try store.load()
XCTAssertEqual(loaded.subscriptions, [record], "订阅记录与刷新状态无损")
XCTAssertEqual(loaded.catalog, catalog)
XCTAssertFalse(
try Data(contentsOf: store.fileURL).range(of: Data("https://".utf8)) != nil,
"订阅 URL 明文不得落盘(D5:只存凭据引用)")
}
// MARK: 节点时间戳(v5,ADR-0031)
func testNodeTimestampsRoundTripLosslessly() throws {
var catalog = ConfigurationCatalog()
let created = Date(timeIntervalSince1970: 1_700_000_000)
let id = try catalog.addServer(
ServerFields(
address: "198.51.100.9",
port: 8388,
encryptionMethod: "aes-256-gcm",
password: "password",
remark: "时间戳"
),
id: NodeID(rawValue: "manual:timestamped"),
now: created
)
try store.save(CatalogDocument(catalog: catalog))
let loaded = try store.load()
let entry = try XCTUnwrap(loaded.catalog.entry(for: id))
XCTAssertEqual(entry.createdAt, created, "创建时间无损")
XCTAssertEqual(entry.updatedAt, created)
}
/// 订阅记录 JSON 用真编码器生成:枚举 Codable 形状是实现细节,不在测试里手写。
private func subscriptionJSON(
id: String, groupID: String, url: String = "https://provider.example/sub"
) throws -> String {
let record = SubscriptionRecord(
id: NodeID(rawValue: id),
groupID: NodeID(rawValue: groupID),
url: url,
status: .never)
return String(data: try JSONEncoder().encode(record), encoding: .utf8)!
}
func testSubscriptionGroupMissingLoadsAsCorrupt() throws {
let recordJSON = try subscriptionJSON(id: "sub-x", groupID: "ghost")
let payload =
"""
{"version": 7, "rootChildren": [], "entries": [], "favoriteIDs": [], "subscriptions": [\(recordJSON)]}
"""
XCTAssertThrowsError(try CatalogFileStore.decode(Data(payload.utf8))) { error in
guard
case CatalogFileStore.PersistenceError.corrupt? = error
as? CatalogFileStore.PersistenceError
else {
return XCTFail("固定分组缺失应报 corrupt,实际 \(error)")
}
}
}
func testDuplicateSubscriptionIDLoadsAsCorrupt() throws {
let first = try subscriptionJSON(id: "sub-x", groupID: "g1", url: "https://provider.example/u")
let second = try subscriptionJSON(id: "sub-x", groupID: "g1", url: "https://provider.example/v")
let payload =
"""
{"version": 7, "rootChildren": [], "entries": [], "favoriteIDs": [], "subscriptions": [\(first), \(second)]}
"""
XCTAssertThrowsError(try CatalogFileStore.decode(Data(payload.utf8))) { error in
guard
case CatalogFileStore.PersistenceError.corrupt? = error
as? CatalogFileStore.PersistenceError
else {
return XCTFail("重复订阅身份应报 corrupt,实际 \(error)")
}
}
}
}