Files

132 lines
6.5 KiB
Swift

import Foundation
import Testing
@testable import ShadowsocksX_NG2
/// Server-form behavior through workflow and draft interfaces.
struct PluginFormCatalogTests {
@Test(arguments: [
PluginCatalogSnapshot.Availability.available, .missing, .notExecutable, .unreadable,
])
@MainActor func formRetainsAvailabilityForNewlySelectedPlugin(
availability: PluginCatalogSnapshot.Availability
) throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
defer { try? FileManager.default.removeItem(at: root) }
let snapshot = PluginCatalogSnapshot(entries: [
.init(
program: "tool", path: "/private/tool", source: .user,
availability: availability, managedInfo: nil)
])
let workflow = makeCatalogWorkflow(
coordinator: CatalogCommitCoordinator(
fileStore: makeTestCatalogStore(fileURL: root.appendingPathComponent("catalog.json")),
runtime: FakeCatalogRuntime()), plugins: snapshot)
let form = workflow.newFormPluginSection(selection: .named(program: "tool"))
#expect(form.programs.first?.source == .user)
#expect(form.programs.first?.availability == availability)
#expect(!form.mappingsUnreadable)
}
@Test @MainActor func missingUnsavedChoiceHasUnresolvedPresentation() {
let plugin = PluginSectionState(
selection: .named(program: "saved"),
programs: [.init(program: "saved", source: .managed, availability: .available)],
mappingsUnreadable: false, optionsPresent: true, options: "flag")
let form = ServerEditForm(
address: "127.0.0.1", port: 8388,
encryptionMethod: "aes-256-gcm", password: "pw", remark: "Original",
plugin: plugin, isEditable: true)
let fields = ServerFormFields()
fields.showServer(.fresh()) { _ in form }
fields.pluginChoice = .named(program: "removed")
fields.pluginOptions.load("host=unsaved")
fields.updatePresentation(ServerFormPresentation(isEditable: true, plugin: plugin))
#expect(fields.pluginChoice == .named(program: "removed"))
#expect(fields.presentation?.plugin.unresolvedProgram(for: fields.pluginChoice) == "removed")
#expect(fields.pluginOptions.composedString == "host=unsaved")
#expect(plugin.unresolvedProgram(for: .none) == nil)
#expect(plugin.unresolvedProgram(for: .named(program: "saved")) == nil)
}
@Test @MainActor func mappingRemovalAndOverrideChangesPreserveServerDraft() async throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
defer { try? FileManager.default.removeItem(at: root) }
let fixture = try makePluginFormFixture(root: root)
let id = try await fixture.workflow.createServer(
ServerEditDraft(
address: "127.0.0.1", port: 8388, encryptionMethod: "aes-256-gcm",
password: "pw", remark: "Original", plugin: .named(program: "tool"), pluginOptions: "flag"),
into: nil)
let fields = ServerFormFields()
fields.showServer(id, load: fixture.workflow.serverEditForm)
fields.remark = "Unsaved"
fields.pluginOptions.load("host=unsaved")
try fixture.plugins.removePlugin(program: "tool")
fields.updatePresentation(
fixture.workflow.serverFormPresentation(for: id), load: fixture.workflow.serverEditForm)
#expect(fields.pluginChoice == .unknown(program: "tool"))
#expect(fields.pluginOptions.composedString == "host=unsaved")
try fixture.plugins.addPlugin(program: "tool", path: fixture.managedBinary.path)
fields.updatePresentation(
fixture.workflow.serverFormPresentation(for: id), load: fixture.workflow.serverEditForm)
#expect(fields.pluginChoice == .named(program: "tool"))
#expect(fields.pluginOptions.composedString == "host=unsaved")
#expect(try fixture.workflow.serverEditForm(for: id)?.plugin.options == "flag")
#expect(fields.remark == "Unsaved")
// An unsaved choice disappearing must retain a selectable unresolved reference.
fields.pluginChoice = .named(program: "tool")
try fixture.plugins.removePlugin(program: "tool")
fields.updatePresentation(
fixture.workflow.serverFormPresentation(for: id), load: fixture.workflow.serverEditForm)
#expect(fields.presentation?.plugin.unresolvedProgram(for: .named(program: "tool")) == "tool")
#expect(
fixture.workflow.newFormPluginSection(selection: .named(program: "tool"))
.unresolvedProgram(for: .named(program: "tool")) == "tool")
#expect(fields.pluginOptions.composedString == "host=unsaved")
fields.pluginChoice = .named(program: "v2ray-plugin")
try fixture.plugins.addPlugin(program: "v2ray-plugin", path: fixture.managedBinary.path)
fields.updatePresentation(
fixture.workflow.serverFormPresentation(for: id), load: fixture.workflow.serverEditForm)
#expect(
fields.presentation?.plugin.programs.first { $0.program == "v2ray-plugin" }?.source == .user)
try fixture.plugins.removePlugin(program: "v2ray-plugin")
fields.updatePresentation(
fixture.workflow.serverFormPresentation(for: id), load: fixture.workflow.serverEditForm)
#expect(
fields.presentation?.plugin.programs.first { $0.program == "v2ray-plugin" }?.source
== .managed)
#expect(fields.pluginChoice == .named(program: "v2ray-plugin"))
#expect(fields.pluginOptions.composedString == "host=unsaved")
#expect(fields.remark == "Unsaved")
}
}
private struct PluginFormFixture {
let managedBinary: URL
let plugins: PluginCatalog
let workflow: CatalogWorkflow
}
@MainActor
private func makePluginFormFixture(root: URL) throws -> PluginFormFixture {
let managedBinary = root.appendingPathComponent("Contents/Helpers/Plugins/v2ray-plugin")
try FileManager.default.createDirectory(
at: managedBinary.deletingLastPathComponent(), withIntermediateDirectories: true)
try Data("#!/bin/sh\n".utf8).write(to: managedBinary)
try FileManager.default.setAttributes(
[.posixPermissions: 0o755], ofItemAtPath: managedBinary.path)
let plugins = PluginCatalog(
store: PluginMappingFileStore(fileURL: root.appendingPathComponent("plugins.json")),
managed: BundleManagedPluginProvider(bundleURL: root), inspector: QuietPluginInspector())
try plugins.addPlugin(program: "tool", path: managedBinary.path)
let workflow = makeCatalogWorkflow(
coordinator: CatalogCommitCoordinator(
fileStore: makeTestCatalogStore(fileURL: root.appendingPathComponent("catalog.json")),
runtime: FakeCatalogRuntime()), plugins: plugins)
return PluginFormFixture(managedBinary: managedBinary, plugins: plugins, workflow: workflow)
}