import XCTest @testable import ShadowsocksX_NG2 /// 受管插件提供:受管集内且 bundle 内可执行文件在位才返回 /// 绝对路径;集外引用与文件缺失都归一为「本版本未提供」(nil → 状态机点名 /// 拒绝)。路径只从静态事实表派生,绝不拼接引用原文。 final class BundleManagedPluginProviderTests: XCTestCase { private var bundleRoot: URL! override func setUpWithError() throws { try super.setUpWithError() bundleRoot = FileManager.default.temporaryDirectory .appendingPathComponent("ssxng-plugin-tests-\(UUID().uuidString)", isDirectory: true) try FileManager.default.createDirectory( at: bundleRoot.appendingPathComponent("Contents/Helpers/Plugins"), withIntermediateDirectories: true) } override func tearDownWithError() throws { try? FileManager.default.removeItem(at: bundleRoot) try super.tearDownWithError() } private var pluginBinaryName: String { ManagedPluginCatalog.plugins[0].program } private var pluginURL: URL { bundleRoot.appendingPathComponent("Contents/Helpers/Plugins/\(pluginBinaryName)") } private func writePluginBinary(permissions: Int) throws { try Data("#!/bin/sh\n".utf8).write(to: pluginURL) try FileManager.default.setAttributes( [.posixPermissions: permissions], ofItemAtPath: pluginURL.path) } private func makeProvider() -> BundleManagedPluginProvider { BundleManagedPluginProvider(bundleURL: bundleRoot) } func testMajorVersionAliasResolvesToSameBundledExecutable() throws { try writePluginBinary(permissions: 0o755) XCTAssertEqual( makeProvider().resolve(program: "v2ray-plugin-v1")?.path, makeProvider().resolve(program: "v2ray-plugin")?.path) XCTAssertNotNil(makeProvider().resolve(program: "v2ray-plugin-v1")?.path) } func testCloakAliasesUseClientBinaryAndPreserveOptions() throws { let client = bundleRoot.appendingPathComponent("Contents/Helpers/Plugins/ck-client") try Data("#!/bin/sh\n".utf8).write(to: client) try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: client.path) let provider = makeProvider() for name in ["cloak", "cloak-v2"] { XCTAssertEqual(provider.resolve(program: name)?.path, client.path) XCTAssertEqual(provider.resolve(program: name)?.mode, .tcpOnly) var catalog = ConfigurationCatalog() var fields = ActivationFixture.plainFields(remark: "Cloak", password: "password") fields.pluginProgram = name fields.pluginOptions = "options" let options = "UID=test;PublicKey=test;ServerName=example.com;EncryptionMethod=plain" fields.password = "pw" fields.pluginOptions = options let id = try catalog.addServer(fields) var machine = ActivationStateMachine() let runtime = try machine.activate( id, in: catalog, plugins: provider, options: RuntimeDocumentOptions(listen: ActivationFixture.listen)) let server = try XCTUnwrap(runtime.document.servers.first) XCTAssertEqual(server.plugin, client.path) XCTAssertEqual(server.pluginOpts, options) } XCTAssertNil(provider.resolve(program: "ck-client")) XCTAssertNil(provider.resolve(program: "Cloak")) } func testReturnsBundleAbsolutePathForProvidedManagedProgram() throws { try writePluginBinary(permissions: 0o755) let path = try XCTUnwrap(makeProvider().resolve(program: pluginBinaryName)?.path) XCTAssertEqual(path, pluginURL.path, "配置生成的 plugin 字段是 bundle 内绝对路径") XCTAssertTrue(path.hasPrefix(bundleRoot.path)) } func testMissingBinaryIsNotProvided() { XCTAssertNil(makeProvider().resolve(program: pluginBinaryName)?.path) } func testNonExecutableBinaryIsNotProvided() throws { try writePluginBinary(permissions: 0o644) XCTAssertNil(makeProvider().resolve(program: pluginBinaryName)?.path) } func testUnmanagedProgramIsNeverProvided() throws { try writePluginBinary(permissions: 0o755) let provider = makeProvider() XCTAssertNil(provider.resolve(program: "obfs-local")?.path, "集外程序名一律未提供") XCTAssertNil(provider.resolve(program: "sslocal")?.path, "非插件的 bundle 内二进制不算插件") XCTAssertNil(provider.resolve(program: "../Helpers/sslocal")?.path, "路径形态引用不拼接") XCTAssertNil(provider.resolve(program: "v2ray-plugin/child")?.path, "子路径引用不拼接") XCTAssertNil(provider.resolve(program: "")?.path) } /// 组合根事实:app bundle(生产默认 provider 的取值来源,经 AppArtifact 按 /// 构建产物定位、不依赖测试宿主)内受管插件在位——防止打包回归静默断开激 /// 活链路(编辑器显示提供而激活拒绝)。 func testHostAppBundleProvidesManagedPlugin() throws { let path = try XCTUnwrap( BundleManagedPluginProvider(bundleURL: AppArtifact.bundleURL) .resolve(program: pluginBinaryName)?.path, "app bundle 未嵌入受管插件(先跑 fetch-external-binaries.sh)") XCTAssertTrue(path.hasSuffix("Contents/Helpers/Plugins/\(pluginBinaryName)")) } func testHostAppBundleProvidesCloakAliasesAndNotices() throws { let bundle = AppArtifact.bundleURL let provider = BundleManagedPluginProvider(bundleURL: bundle) let path = try XCTUnwrap(provider.resolve(program: "cloak")?.path) XCTAssertTrue(path.hasSuffix("Contents/Helpers/Plugins/ck-client")) XCTAssertEqual(provider.resolve(program: "cloak-v2")?.path, path) let catalog = try BundledDependencyCatalog(bundleURL: bundle) let dependency = try XCTUnwrap(catalog.dependencies.first { $0.id == "ck-client" }) for reference in dependency.licenses + dependency.notices { XCTAssertFalse(try catalog.text(at: reference).isEmpty) } } /// 与状态机缝的契约:文件缺失 → provider nil → 激活以 pluginNotProvided /// 原子拒绝(app 降级丢插件时同一语义)。 func testActivationRejectsMissingPluginByNamedReason() throws { var catalog = ConfigurationCatalog() var fields = ActivationFixture.plainFields(remark: "带插件", password: "password") fields.pluginProgram = pluginBinaryName fields.password = "pw" let id = try catalog.addServer(fields) var machine = ActivationStateMachine() ActivationFixture.assertThrows( .invalidLeaf(node: id, reason: .pluginNotProvided(program: pluginBinaryName)) ) { try machine.activate( id, in: catalog, plugins: makeProvider(), options: RuntimeDocumentOptions(listen: ActivationFixture.listen)) } } /// 在位 → 派生文档携带 bundle 内绝对路径(与 opts 解析),激活成功。 func testActivationDerivesBundlePathWhenProvided() throws { try writePluginBinary(permissions: 0o755) var catalog = ConfigurationCatalog() var fields = ActivationFixture.plainFields(remark: "带插件", password: "password") fields.pluginProgram = pluginBinaryName fields.password = "pw" let id = try catalog.addServer(fields) var machine = ActivationStateMachine() let configuration = try machine.activate( id, in: catalog, plugins: makeProvider(), options: RuntimeDocumentOptions(listen: ActivationFixture.listen)) let server = try XCTUnwrap(configuration.document.servers.first) XCTAssertEqual(server.plugin, pluginURL.path) XCTAssertNil(server.pluginOpts, "无参数引用时整体省略 pluginOpts") } }