Files
ShadowsocksX-NG/ShadowsocksX-NG2/Tests/Support/DiagnosticsWorkflowFixture.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?
}