Skip to content

Commit 31235a1

Browse files
committed
Test import, delete and validation for the imported-plugin store
1 parent 53ab451 commit 31235a1

1 file changed

Lines changed: 46 additions & 75 deletions

File tree

Tests/BluetoothExplorerPluginEngineTests/PluginDirectoryTests.swift

Lines changed: 46 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
// PluginDirectoryTests.swift
33
// BluetoothExplorerPluginEngineTests
44
//
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.
5+
// Covers the Documents-backed store for user-imported plugins: import validation, that a bad
6+
// import writes nothing, deletion, and identifier-to-folder-name sanitising.
77
//
8-
// Every test runs against a temporary directory — never the real Documents folder.
8+
// Bundled plugins are not stored here (they are referenced read-only from the app bundle), so
9+
// these tests stage a bundled plugin's files into a scratch directory to stand in for a user's
10+
// imported file. Every test runs against a temporary directory — never the real Documents folder.
911
//
1012

1113
import Foundation
@@ -23,76 +25,33 @@ struct PluginDirectoryTests {
2325
return directory
2426
}
2527

26-
private func bundledManifestCount() -> Int {
27-
let urls = PluginEngineResources.bundle.urls(
28-
forResourcesWithExtension: "json", subdirectory: "Plugins") ?? []
29-
return urls.map { $0 as URL }
28+
/// Copy one bundled plugin's manifest and module into a fresh scratch directory, standing in
29+
/// for a file the user picked from outside the app. Returns the staged manifest URL.
30+
private func stageBundledPlugin() throws -> URL {
31+
let manifestURLs = (PluginEngineResources.bundle.urls(
32+
forResourcesWithExtension: "json", subdirectory: "Plugins") ?? [])
33+
.map { $0 as URL }
3034
.filter { $0.lastPathComponent.hasSuffix(PluginDirectory.manifestSuffix) }
31-
.count
35+
let manifestURL = try #require(manifestURLs.first)
36+
let manifest = try JSONDecoder().decode(
37+
PluginManifest.self, from: try Data(contentsOf: manifestURL))
38+
let moduleURL = manifestURL.deletingLastPathComponent().appendingPathComponent(manifest.module)
39+
40+
let scratch = URL(fileURLWithPath: NSTemporaryDirectory())
41+
.appendingPathComponent("bleplug-stage-\(UUID().uuidString)", isDirectory: true)
42+
try FileManager.default.createDirectory(at: scratch, withIntermediateDirectories: true)
43+
let stagedManifest = scratch.appendingPathComponent(manifestURL.lastPathComponent)
44+
try Data(contentsOf: manifestURL).write(to: stagedManifest)
45+
try Data(contentsOf: moduleURL).write(to: scratch.appendingPathComponent(manifest.module))
46+
return stagedManifest
3247
}
3348

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")
49+
@Test("Importing a valid plugin copies it into the store and loads")
8650
func importValidPlugin() throws {
87-
let source = try makeTemporaryDirectory()
8851
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)
52+
defer { try? FileManager.default.removeItem(at: destination.url) }
53+
let staged = try stageBundledPlugin()
54+
defer { try? FileManager.default.removeItem(at: staged.deletingLastPathComponent()) }
9655

9756
let manifest = try destination.importPlugin(manifestURL: staged)
9857
#expect(destination.installedManifestURLs().count == 1)
@@ -104,14 +63,10 @@ struct PluginDirectoryTests {
10463

10564
@Test("Importing a plugin with a wrong hash fails and writes nothing")
10665
func importRejectsBadHash() throws {
107-
let source = try makeTemporaryDirectory()
10866
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)
67+
defer { try? FileManager.default.removeItem(at: destination.url) }
68+
let staged = try stageBundledPlugin()
69+
defer { try? FileManager.default.removeItem(at: staged.deletingLastPathComponent()) }
11570

11671
// Corrupt the manifest's hash in place.
11772
let data = try Data(contentsOf: staged)
@@ -126,6 +81,22 @@ struct PluginDirectoryTests {
12681
#expect(destination.installedManifestURLs().isEmpty)
12782
}
12883

84+
@Test("An imported plugin can be removed")
85+
func removeImportedPlugin() throws {
86+
let destination = try makeTemporaryDirectory()
87+
defer { try? FileManager.default.removeItem(at: destination.url) }
88+
let staged = try stageBundledPlugin()
89+
defer { try? FileManager.default.removeItem(at: staged.deletingLastPathComponent()) }
90+
91+
let manifest = try destination.importPlugin(manifestURL: staged)
92+
#expect(destination.installedManifestURLs().count == 1)
93+
94+
try destination.remove(identifier: manifest.identifier)
95+
#expect(destination.installedManifestURLs().isEmpty)
96+
// Removing something that is not there is a no-op, not an error.
97+
try destination.remove(identifier: manifest.identifier)
98+
}
99+
129100
@Test("Identifiers map to filesystem-safe folder names")
130101
func folderNames() {
131102
#expect(PluginDirectory.folderName(for: "org.pureswift.plugin.gatt-time") == "org.pureswift.plugin.gatt-time")

0 commit comments

Comments
 (0)