mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-08 10:44:50 +00:00
170 lines
6.4 KiB
Swift
170 lines
6.4 KiB
Swift
import XCTest
|
|
|
|
@testable import ShadowsocksX_NG2
|
|
|
|
/// 诊断工作流测试共享夹具:runtime 事实替身、投毒事件集、聚合
|
|
/// 目录事实与 workflow 构造(事实源全部注入,时钟固定,轮询 10ms)。
|
|
@MainActor
|
|
class DiagnosticsWorkflowTestCase: XCTestCase {
|
|
let password = "SECRET-PASSWORD"
|
|
let address = "203.0.113.7"
|
|
let remark = "香港 01"
|
|
let home = "/Users/SECRETUSER"
|
|
let lanAddress = "192.0.2.77"
|
|
|
|
/// 投毒事件 detail:白名单必须整体清洗,不得进入报告。
|
|
let poisonDetails = [
|
|
"SECRET-PERSIST-DETAIL", "SECRET-PROBE-DETAIL",
|
|
"SECRET-LISTEN-DETAIL", "SECRET-AGENT-DETAIL", "SECRET-UNREADABLE-DETAIL",
|
|
]
|
|
|
|
lazy var secrets: [String] = {
|
|
[password, "SECRET-PLUGIN-OPTIONS", address, remark, home, lanAddress] + poisonDetails
|
|
}()
|
|
|
|
var workDir: URL!
|
|
var facts: FakeRuntimeFacts!
|
|
var events: RuntimeEventStore!
|
|
|
|
override func setUp() async throws {
|
|
try await super.setUp()
|
|
workDir = FileManager.default.temporaryDirectory
|
|
.appendingPathComponent("ssxng-diagwf-\(UUID().uuidString)", isDirectory: true)
|
|
try? FileManager.default.createDirectory(at: workDir, withIntermediateDirectories: true)
|
|
facts = FakeRuntimeFacts()
|
|
events = RuntimeEventStore()
|
|
// 导出完成事件经 RuntimeLog 发射:测试期间把替身缓冲注册为接收缝。
|
|
RuntimeLog.setSink(events)
|
|
}
|
|
|
|
override func tearDown() async throws {
|
|
RuntimeLog.setSink(RuntimeEventStore.shared)
|
|
try? FileManager.default.removeItem(at: workDir)
|
|
try await super.tearDown()
|
|
}
|
|
|
|
/// 标准 workflow:事实源注入替身、固定时钟(报告生成时间可精确断言)。
|
|
func makeWorkflow(
|
|
catalog: @escaping @MainActor () -> DiagnosticCatalogFacts? = { nil },
|
|
agentLog: @escaping () -> String? = { nil },
|
|
fileFacts: @escaping @MainActor () -> [DiagnosticFileFacts] = { [] },
|
|
managedPlugins: @escaping @MainActor () -> [DiagnosticPluginFacts] = { [] },
|
|
homePath: @escaping () -> String? = { nil },
|
|
render: @escaping (DiagnosticSnapshot) -> String? = {
|
|
DiagnosticReportBuilder.markdown(from: $0)
|
|
},
|
|
capture: ((DiagnosticSnapshot) -> Void)? = nil
|
|
) -> DiagnosticsWorkflow {
|
|
DiagnosticsWorkflow(
|
|
runtimeFacts: facts,
|
|
events: events,
|
|
catalogFacts: catalog,
|
|
agentLogTail: {
|
|
agentLog().map { DiagnosticLogTail(text: $0, fileID: 0, offset: 0) }
|
|
},
|
|
fileFacts: fileFacts,
|
|
managedPlugins: managedPlugins,
|
|
homePath: homePath,
|
|
clock: { Date(timeIntervalSince1970: 1_758_000_000) },
|
|
reportRendering: { snapshot in
|
|
capture?(snapshot)
|
|
return render(snapshot)
|
|
},
|
|
pollInterval: .milliseconds(10))
|
|
}
|
|
|
|
func waitUntil(
|
|
_ condition: @autoclosure () -> Bool,
|
|
timeout: TimeInterval = 2,
|
|
file: StaticString = #filePath,
|
|
line: UInt = #line
|
|
) async {
|
|
let deadline = Date().addingTimeInterval(timeout)
|
|
while !condition() && Date() < deadline {
|
|
try? await Task.sleep(for: .milliseconds(10))
|
|
}
|
|
XCTAssertTrue(condition(), "等待条件超时", file: file, line: line)
|
|
}
|
|
|
|
func assertNoSecrets(
|
|
_ text: String, file: StaticString = #filePath, line: UInt = #line
|
|
) {
|
|
for secret in secrets {
|
|
XCTAssertFalse(
|
|
text.contains(secret),
|
|
"诊断报告不得包含敏感值「\(secret)」\n实际:\(text)",
|
|
file: file, line: line)
|
|
}
|
|
XCTAssertFalse(text.contains("{"), "诊断报告不得携带 JSON 内容", file: file, line: line)
|
|
}
|
|
|
|
/// 投毒目录:地址、备注、分组名全为敏感值(聚合后这些值在结构上进不了
|
|
/// facts)。
|
|
func poisonedCatalogFacts() throws -> DiagnosticCatalogFacts {
|
|
var catalog = ConfigurationCatalog()
|
|
let group = try catalog.addGroup("SECRET-GROUP")
|
|
_ = try catalog.addServer(
|
|
ServerFields(
|
|
address: address, port: 8388, encryptionMethod: "aes-256-gcm",
|
|
password: password,
|
|
remark: remark,
|
|
pluginProgram: "v2ray-plugin",
|
|
pluginOptions: "SECRET-PLUGIN-OPTIONS"),
|
|
to: group)
|
|
let subscriptionGroup = try catalog.addGroup("SECRET-GROUP-sub", source: .subscription)
|
|
_ = try catalog.addServer(
|
|
ServerFields(
|
|
address: address, port: 8389, encryptionMethod: "chacha20-ietf-poly1305",
|
|
password: password, remark: remark),
|
|
source: .subscription,
|
|
to: subscriptionGroup)
|
|
return DiagnosticCatalogFacts(counts: DiagnosticReportBuilder.counts(in: catalog))
|
|
}
|
|
|
|
/// 投毒事件集:白名单内类别 + 自由 detail 携带敏感值。
|
|
func appendPoisonedEvents() {
|
|
let stamp = Date(timeIntervalSince1970: 1_758_000_000)
|
|
events.append(event: .contractWritten(serverCount: 3), timestamp: stamp)
|
|
events.append(
|
|
event: .runtimePersistFailed(detail: "\(home): SECRET-PERSIST-DETAIL"), timestamp: stamp)
|
|
events.append(
|
|
event: .endpointProbeFailed(host: lanAddress, port: 11086, detail: "SECRET-PROBE-DETAIL"),
|
|
timestamp: stamp)
|
|
events.append(event: .listenNotEstablished(detail: "SECRET-LISTEN-DETAIL"), timestamp: stamp)
|
|
events.append(
|
|
event: .agentRegisterFailed(detail: "SECRET-AGENT-DETAIL"), timestamp: stamp)
|
|
events.append(
|
|
event: .settingsUnreadable(detail: "SECRET-UNREADABLE-DETAIL"), timestamp: stamp)
|
|
// 激活原因是领域点名文案(显式白名单决策):可在场,但只允许
|
|
// 这类固定文本。
|
|
events.append(
|
|
event: .activationFailed(reason: "激活目标 leaf-9 的插件 simple-obfs 未随 app 提供"),
|
|
timestamp: stamp)
|
|
}
|
|
|
|
func reportText(_ outcome: DiagnosticReportOutcome) throws -> String {
|
|
guard case .ready(let draft) = outcome else {
|
|
XCTFail("应产出 ready 报告,实际 \(outcome)")
|
|
throw XCTSkip("无报告文本")
|
|
}
|
|
return try XCTUnwrap(String(data: draft.data, encoding: .utf8))
|
|
}
|
|
|
|
static func reportTimestamp(_ date: Date) -> String {
|
|
let formatter = DateFormatter()
|
|
formatter.locale = Locale(identifier: "en_US_POSIX")
|
|
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
|
return formatter.string(from: date)
|
|
}
|
|
}
|
|
|
|
/// runtime 事实替身:可编程的安全事实值。
|
|
@MainActor
|
|
final class FakeRuntimeFacts: ProxyRuntimeDiagnosticFacts {
|
|
var proxyState: DiagnosticProxyState = .off
|
|
var hasActiveTarget = false
|
|
var listen = RuntimeListenFacts(listen: ProxyListeningConfiguration())
|
|
var contractSummary: String?
|
|
var customRuleSummary: CustomRuleSummary?
|
|
}
|