Skip to content

Commit d2b825b

Browse files
committed
Test plugin install, idempotence, deletion and import
1 parent e5c1f3a commit d2b825b

1 file changed

Lines changed: 135 additions & 0 deletions

File tree

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
//
2+
// PluginDirectoryTests.swift
3+
// BluetoothExplorerPluginEngineTests
4+
//
5+
// Covers the Documents-backed plugin store: first-launch install of the bundled plugins,
6+
// idempotence across launches, that a deleted bundled plugin stays deleted, and import validation.
7+
//
8+
// Every test runs against a temporary directory — never the real Documents folder.
9+
//
10+
11+
import Foundation
12+
import Testing
13+
@testable import BluetoothExplorerPluginEngine
14+
15+
@Suite("Plugin directory")
16+
struct PluginDirectoryTests {
17+
18+
private func makeTemporaryDirectory() throws -> PluginDirectory {
19+
let url = URL(fileURLWithPath: NSTemporaryDirectory())
20+
.appendingPathComponent("bleplug-store-\(UUID().uuidString)", isDirectory: true)
21+
let directory = PluginDirectory(url: url)
22+
try directory.createIfNeeded()
23+
return directory
24+
}
25+
26+
private func bundledManifestCount() -> Int {
27+
let urls = PluginEngineResources.bundle.urls(
28+
forResourcesWithExtension: "json", subdirectory: "Plugins") ?? []
29+
return urls.map { $0 as URL }
30+
.filter { $0.lastPathComponent.hasSuffix(PluginDirectory.manifestSuffix) }
31+
.count
32+
}
33+
34+
@Test("First launch installs every bundled plugin")
35+
func firstLaunchInstalls() throws {
36+
let directory = try makeTemporaryDirectory()
37+
defer { try? FileManager.default.removeItem(at: directory.url) }
38+
39+
#expect(directory.isFirstLaunch)
40+
let installed = try directory.installBundledPlugins(from: PluginEngineResources.bundle)
41+
#expect(installed.count == bundledManifestCount())
42+
#expect(installed.isEmpty == false)
43+
#expect(directory.isFirstLaunch == false)
44+
#expect(directory.installedManifestURLs().count == installed.count)
45+
}
46+
47+
@Test("Installed plugins load from the directory")
48+
func installedPluginsLoad() throws {
49+
let directory = try makeTemporaryDirectory()
50+
defer { try? FileManager.default.removeItem(at: directory.url) }
51+
try directory.installBundledPlugins(from: PluginEngineResources.bundle)
52+
53+
for manifestURL in directory.installedManifestURLs() {
54+
// Hash verification is on: a copy that lost bytes would fail here.
55+
_ = try PluginLoader.load(manifestURL: manifestURL, verifyHash: true)
56+
}
57+
}
58+
59+
@Test("A second launch installs nothing new")
60+
func secondLaunchIsIdempotent() throws {
61+
let directory = try makeTemporaryDirectory()
62+
defer { try? FileManager.default.removeItem(at: directory.url) }
63+
64+
let first = try directory.installBundledPlugins(from: PluginEngineResources.bundle)
65+
let second = try directory.installBundledPlugins(from: PluginEngineResources.bundle)
66+
#expect(second.isEmpty, "reinstalled on second launch: \(second)")
67+
#expect(directory.installedManifestURLs().count == first.count)
68+
}
69+
70+
@Test("A deleted bundled plugin is not resurrected on the next launch")
71+
func deletedPluginStaysDeleted() throws {
72+
let directory = try makeTemporaryDirectory()
73+
defer { try? FileManager.default.removeItem(at: directory.url) }
74+
75+
let installed = try directory.installBundledPlugins(from: PluginEngineResources.bundle)
76+
let victim = try #require(installed.first)
77+
try directory.remove(identifier: victim)
78+
#expect(directory.installedManifestURLs().count == installed.count - 1)
79+
80+
let afterRelaunch = try directory.installBundledPlugins(from: PluginEngineResources.bundle)
81+
#expect(afterRelaunch.isEmpty, "deleted plugin came back: \(afterRelaunch)")
82+
#expect(directory.installedManifestURLs().count == installed.count - 1)
83+
}
84+
85+
@Test("Importing a valid plugin copies it into the store")
86+
func importValidPlugin() throws {
87+
let source = try makeTemporaryDirectory()
88+
let destination = try makeTemporaryDirectory()
89+
defer {
90+
try? FileManager.default.removeItem(at: source.url)
91+
try? FileManager.default.removeItem(at: destination.url)
92+
}
93+
// Stage a plugin outside the store, the way a user's file would arrive.
94+
try source.installBundledPlugins(from: PluginEngineResources.bundle)
95+
let staged = try #require(source.installedManifestURLs().first)
96+
97+
let manifest = try destination.importPlugin(manifestURL: staged)
98+
#expect(destination.installedManifestURLs().count == 1)
99+
// The imported copy must load and verify against its own hash.
100+
let installed = try #require(destination.installedManifestURLs().first)
101+
let loaded = try PluginLoader.load(manifestURL: installed, verifyHash: true)
102+
#expect(loaded.manifest.identifier == manifest.identifier)
103+
}
104+
105+
@Test("Importing a plugin with a wrong hash fails and writes nothing")
106+
func importRejectsBadHash() throws {
107+
let source = try makeTemporaryDirectory()
108+
let destination = try makeTemporaryDirectory()
109+
defer {
110+
try? FileManager.default.removeItem(at: source.url)
111+
try? FileManager.default.removeItem(at: destination.url)
112+
}
113+
try source.installBundledPlugins(from: PluginEngineResources.bundle)
114+
let staged = try #require(source.installedManifestURLs().first)
115+
116+
// Corrupt the manifest's hash in place.
117+
let data = try Data(contentsOf: staged)
118+
var json = try #require(try JSONSerialization.jsonObject(with: data) as? [String: Any])
119+
json["sha256"] = String(repeating: "0", count: 64)
120+
try JSONSerialization.data(withJSONObject: json).write(to: staged)
121+
122+
#expect(throws: (any Error).self) {
123+
_ = try destination.importPlugin(manifestURL: staged)
124+
}
125+
// Validation happens before anything is written, so the store stays empty.
126+
#expect(destination.installedManifestURLs().isEmpty)
127+
}
128+
129+
@Test("Identifiers map to filesystem-safe folder names")
130+
func folderNames() {
131+
#expect(PluginDirectory.folderName(for: "org.pureswift.plugin.gatt-time") == "org.pureswift.plugin.gatt-time")
132+
#expect(PluginDirectory.folderName(for: "a/b:c") == "a_b_c")
133+
#expect(PluginDirectory.folderName(for: "").isEmpty == false)
134+
}
135+
}

0 commit comments

Comments
 (0)