Files
ggbond268--mactools/Tests/Core/Plugins/Dynamic/PluginPackageResolverTests.swift
wehub-resource-sync 1d1286fadb
Build / Build and test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:18:38 +08:00

244 lines
9.5 KiB
Swift

import CryptoKit
import Foundation
import XCTest
@testable import MacTools
@MainActor
final class PluginPackageResolverTests: XCTestCase {
private var temporaryRoot: URL!
override func setUpWithError() throws {
temporaryRoot = FileManager.default.temporaryDirectory
.appendingPathComponent("PluginPackageResolverTests-\(UUID().uuidString)", isDirectory: true)
try FileManager.default.createDirectory(at: temporaryRoot, withIntermediateDirectories: true)
}
override func tearDownWithError() throws {
if let temporaryRoot {
try? FileManager.default.removeItem(at: temporaryRoot)
}
temporaryRoot = nil
}
func testLocalPackageIsCopiedAndValidated() async throws {
let packageURL = try makePackage(id: "com.example.demo", version: "1.0.0")
let entry = try makeEntry(for: packageURL, id: "com.example.demo", version: "1.0.0")
let resolver = PluginPackageResolver(
temporaryDirectory: temporaryRoot.appendingPathComponent("Temporary", isDirectory: true)
)
let resolvedURL = try await resolver.resolvePackage(for: entry)
XCTAssertTrue(FileManager.default.fileExists(atPath: resolvedURL.path))
XCTAssertNotEqual(resolvedURL.path, packageURL.path)
XCTAssertTrue(FileManager.default.fileExists(atPath: packageURL.path))
}
func testRejectsChecksumMismatch() async throws {
let packageURL = try makePackage(id: "com.example.demo", version: "1.0.0")
let entry = PluginCatalogEntry(
id: "com.example.demo",
displayName: "Demo",
summary: "示例插件",
version: "1.0.0",
minimumHostVersion: "0.1.0",
package: PluginCatalogPackage(
url: packageURL,
sha256: String(repeating: "b", count: 64),
size: try PluginPackageResolver.packageMetrics(for: packageURL).size
)
)
let resolver = PluginPackageResolver(
temporaryDirectory: temporaryRoot.appendingPathComponent("Temporary", isDirectory: true)
)
do {
_ = try await resolver.resolvePackage(for: entry)
XCTFail("Expected checksum mismatch")
} catch let error as PluginPackageResolverError {
guard case .checksumMismatch = error else {
return XCTFail("Unexpected error: \(error)")
}
}
}
func testRejectsManifestMismatch() async throws {
let packageURL = try makePackage(id: "com.example.demo", version: "1.0.0")
let entry = try makeEntry(for: packageURL, id: "com.example.other", version: "1.0.0")
let resolver = PluginPackageResolver(
temporaryDirectory: temporaryRoot.appendingPathComponent("Temporary", isDirectory: true)
)
do {
_ = try await resolver.resolvePackage(for: entry)
XCTFail("Expected manifest mismatch")
} catch {
XCTAssertEqual(error as? PluginPackageResolverError, .manifestMismatch(field: "id"))
}
}
func testRejectsReleaseChannelMismatch() async throws {
let packageURL = try makePackage(id: "com.example.demo", version: "1.0.0")
let entry = try makeEntry(
for: packageURL,
id: "com.example.demo",
version: "1.0.0",
releaseChannel: "beta"
)
let resolver = PluginPackageResolver(
temporaryDirectory: temporaryRoot.appendingPathComponent("Temporary", isDirectory: true)
)
do {
_ = try await resolver.resolvePackage(for: entry)
XCTFail("Expected release channel mismatch")
} catch {
XCTAssertEqual(error as? PluginPackageResolverError, .manifestMismatch(field: "releaseChannel"))
}
}
func testZipPackageExpandsToMactoolsPluginDirectory() async throws {
let packageURL = try makePackage(id: "com.example.demo", version: "1.0.0")
let zipURL = temporaryRoot.appendingPathComponent("Demo.mactoolsplugin.zip")
try zipPackage(packageURL, to: zipURL)
let entry = try makeEntry(for: zipURL, id: "com.example.demo", version: "1.0.0")
let resolver = PluginPackageResolver(
temporaryDirectory: temporaryRoot.appendingPathComponent("Temporary", isDirectory: true)
)
let resolvedURL = try await resolver.resolvePackage(for: entry)
XCTAssertEqual(resolvedURL.pathExtension, "mactoolsplugin")
XCTAssertTrue(FileManager.default.fileExists(atPath: resolvedURL.appendingPathComponent("plugin.json").path))
}
func testDirectoryPackageMetricsIncludeBundleContents() throws {
let packageURL = try makePackage(id: "com.example.demo", version: "1.0.0")
let executableURL = packageURL
.appendingPathComponent("Demo.bundle", isDirectory: true)
.appendingPathComponent("Contents", isDirectory: true)
.appendingPathComponent("MacOS", isDirectory: true)
.appendingPathComponent("Demo", isDirectory: false)
try FileManager.default.createDirectory(
at: executableURL.deletingLastPathComponent(),
withIntermediateDirectories: true
)
try Data("before".utf8).write(to: executableURL)
let before = try PluginPackageResolver.packageMetrics(for: packageURL)
try Data("after".utf8).write(to: executableURL)
let after = try PluginPackageResolver.packageMetrics(for: packageURL)
XCTAssertNotEqual(before.sha256, after.sha256)
}
func testDirectoryPackageMetricsUseStableRelativePathOrder() throws {
let packageURL = try makePackage(id: "com.example.demo", version: "1.0.0")
let contentsURL = packageURL
.appendingPathComponent("Demo.bundle", isDirectory: true)
.appendingPathComponent("Contents", isDirectory: true)
let codeSignatureURL = contentsURL
.appendingPathComponent("_CodeSignature", isDirectory: true)
.appendingPathComponent("CodeResources", isDirectory: false)
let infoPlistURL = contentsURL.appendingPathComponent("Info.plist", isDirectory: false)
try FileManager.default.createDirectory(
at: codeSignatureURL.deletingLastPathComponent(),
withIntermediateDirectories: true
)
try Data("signature".utf8).write(to: codeSignatureURL)
try Data("info".utf8).write(to: infoPlistURL)
let metrics = try PluginPackageResolver.packageMetrics(for: packageURL)
let expected = stableDirectoryMetrics(
root: packageURL,
files: [
"Demo.bundle/Contents/Info.plist",
"Demo.bundle/Contents/_CodeSignature/CodeResources",
"plugin.json",
]
)
XCTAssertEqual(metrics, expected)
}
private func makePackage(id: String, version: String) throws -> URL {
let packageURL = temporaryRoot
.appendingPathComponent("\(id)-\(version)", isDirectory: true)
.appendingPathExtension("mactoolsplugin")
let bundleURL = packageURL.appendingPathComponent("Demo.bundle", isDirectory: true)
try FileManager.default.createDirectory(at: bundleURL, withIntermediateDirectories: true)
let manifest = PluginPackageManifest(
id: id,
displayName: "Demo",
version: version,
minHostVersion: "0.1.0",
bundleRelativePath: "Demo.bundle"
)
let data = try JSONEncoder().encode(manifest)
try data.write(to: packageURL.appendingPathComponent("plugin.json"))
return packageURL
}
private func makeEntry(
for packageURL: URL,
id: String,
version: String,
releaseChannel: String? = nil
) throws -> PluginCatalogEntry {
let metrics = try PluginPackageResolver.packageMetrics(for: packageURL)
return PluginCatalogEntry(
id: id,
displayName: "Demo",
summary: "示例插件",
version: version,
minimumHostVersion: "0.1.0",
package: PluginCatalogPackage(
url: packageURL,
sha256: metrics.sha256,
size: metrics.size
),
releaseChannel: releaseChannel
)
}
private func zipPackage(_ packageURL: URL, to zipURL: URL) throws {
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/ditto")
process.arguments = [
"-c",
"-k",
"--sequesterRsrc",
"--keepParent",
packageURL.path,
zipURL.path
]
try process.run()
process.waitUntilExit()
XCTAssertEqual(process.terminationStatus, 0)
}
private func stableDirectoryMetrics(root: URL, files: [String]) -> PluginPackageMetrics {
var hasher = SHA256()
var size: Int64 = 0
for relativePath in files.sorted(by: { $0.utf8.lexicographicallyPrecedes($1.utf8) }) {
let fileURL = root.appendingPathComponent(relativePath)
let data = (try? Data(contentsOf: fileURL)) ?? Data()
hasher.update(data: Data(relativePath.utf8))
hasher.update(data: Data([0]))
hasher.update(data: data)
hasher.update(data: Data([0]))
size += Int64(data.count)
}
let digest = hasher.finalize()
return PluginPackageMetrics(
size: size,
sha256: digest.map { String(format: "%02x", $0) }.joined()
)
}
}