Files
ShadowsocksX-NG/ShadowsocksX-NG2/Tests/Catalog/Workflow/CatalogWorkflowCreateServerTests.swift

125 lines
5.3 KiB
Swift

import XCTest
@testable import ShadowsocksX_NG2
/// 表单新建手动服务器的点名校验用例(标签 + 草稿 + 预期 typed error)。
private struct InvalidCreateDraftCase {
let label: String
let draft: ServerEditDraft
let expected: ServerFormError
init(_ label: String, _ draft: ServerEditDraft, _ expected: ServerFormError) {
self.label = label
self.draft = draft
self.expected = expected
}
}
/// 表单新建手动服务器命令(`createServer`)的 UI-facing interface 测试:
/// 落点与身份、凭据入存储、点名校验拒绝与插件选择落盘,语义与编辑提交对称。
@MainActor
final class CatalogWorkflowCreateServerTests: XCTestCase {
private var workDir: URL!
private var fileURL: URL!
private var runtime: FakeCatalogRuntime!
var workflow: CatalogWorkflow!
override func setUp() async throws {
try await super.setUp()
// 专用工作目录:写入器会把父目录强制 0700(不得指向临时根)。
workDir = FileManager.default.temporaryDirectory
.appendingPathComponent("catalog-create-server-tests-\(UUID().uuidString)", isDirectory: true)
try FileManager.default.createDirectory(at: workDir, withIntermediateDirectories: true)
fileURL = workDir.appendingPathComponent("catalog.json")
runtime = FakeCatalogRuntime()
runtime.hasActiveTarget = true
workflow = makeWorkflow()
}
override func tearDown() async throws {
try? FileManager.default.removeItem(at: workDir)
try await super.tearDown()
}
private func makeWorkflow() -> CatalogWorkflow {
let coordinator = CatalogCommitCoordinator(
fileStore: makeTestCatalogStore(fileURL: fileURL), runtime: runtime)
return makeCatalogWorkflow(
coordinator: coordinator,
plugins: NoManagedPluginProvider())
}
func testCreateServerAddsManualLeafWithCredentialAndReturnsIdentity() async throws {
let groupID = try await workflow.createGroup(named: "手动组", into: nil)
let id = try await workflow.createServer(
ServerEditDraft(
address: "203.0.113.10", port: 8389, encryptionMethod: "aes-256-gcm",
password: "表单密码", remark: "新来的", plugin: .none, pluginOptions: nil),
into: groupID)
let node = try XCTUnwrap(workflow.tree.node(withID: id))
XCTAssertFalse(node.isGroup)
XCTAssertTrue(node.isManual)
XCTAssertEqual(node.parentID, groupID)
let form = try XCTUnwrap(try workflow.serverEditForm(for: id))
XCTAssertEqual(form.address, "203.0.113.10")
XCTAssertEqual(form.port, 8389)
XCTAssertEqual(form.remark, "新来的")
XCTAssertEqual(form.encryptionMethod, "aes-256-gcm")
XCTAssertEqual(form.password, "表单密码", "密码作为凭据写入,目录持引用")
XCTAssertTrue(node.invalidReasons.isEmpty)
}
func testCreateServerRejectsInvalidDraftsLeavingCatalogAndCredentialsUntouched() async throws {
let groupID = try await workflow.createGroup(named: "组", into: nil)
for caseItem in Self.invalidDraftCases {
await expectThrowsAsync(
{ _ = try await workflow.createServer(caseItem.draft, into: groupID) },
onThrow: { error in
XCTAssertEqual(error as? ServerFormError, caseItem.expected, caseItem.label)
})
}
XCTAssertEqual(workflow.tree.node(withID: groupID)?.childCount, 0, "校验失败不进目录")
}
func testCreateServerPluginSelectionStoresProgramAndOptions() async throws {
let id = try await workflow.createServer(
ServerEditDraft(
address: "203.0.113.10", port: 8388, encryptionMethod: "aes-256-gcm",
password: "密码", remark: "插件服务器", plugin: .named(program: "v2ray-plugin"),
pluginOptions: "mode=websocket;host=example.com"),
into: nil)
let plugin = try XCTUnwrap(try workflow.serverEditForm(for: id)?.plugin)
XCTAssertEqual(plugin.selection, .named(program: "v2ray-plugin"))
XCTAssertTrue(plugin.optionsPresent)
XCTAssertEqual(plugin.options, "mode=websocket;host=example.com")
}
/// 表单校验用例表:与 `updateServer` 同规则的点名拒绝(含受管集外插件)。
private static var invalidDraftCases: [InvalidCreateDraftCase] {
func draft(
address: String = "203.0.113.10", port: Int = 8388,
encryptionMethod: String = "aes-256-gcm", password: String = "密码",
plugin: PluginSelection = .none
) -> ServerEditDraft {
ServerEditDraft(
address: address, port: port, encryptionMethod: encryptionMethod, password: password,
remark: "", plugin: plugin, pluginOptions: nil)
}
return [
InvalidCreateDraftCase("空地址", draft(address: " "), .invalidAddress),
InvalidCreateDraftCase("端口下界", draft(port: 0), .invalidPort),
InvalidCreateDraftCase("端口上界", draft(port: 65_536), .invalidPort),
InvalidCreateDraftCase("缺加密方式", draft(encryptionMethod: " "), .missingEncryptionMethod),
InvalidCreateDraftCase(
"不支持加密方式", draft(encryptionMethod: "future-cipher"),
.unsupportedEncryptionMethod("future-cipher")),
InvalidCreateDraftCase("空密码", draft(password: ""), .invalidPassword),
InvalidCreateDraftCase(
"受管集外插件", draft(plugin: .named(program: "future-plugin")),
.pluginUnknown("future-plugin")),
]
}
}