Skip to content

Commit 205d698

Browse files
committed
test(plugins): add PluginSettingsStore on-disk JSON tests
- verify load returns empty for missing or malformed files - test save and load round-trip JSON data - ensure save overwrites previous data correctly - sanitize plugin IDs to prevent path traversal and nested directories
1 parent 05c8b68 commit 205d698

1 file changed

Lines changed: 84 additions & 0 deletions

File tree

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import Foundation
2+
import Testing
3+
@testable import ASCPlugin
4+
5+
@Suite("PluginSettingsStore — on-disk JSON settings")
6+
struct PluginSettingsStoreTests {
7+
private static func makeTempDir() -> URL {
8+
let url = FileManager.default.temporaryDirectory
9+
.appendingPathComponent("plugin-settings-test-\(UUID().uuidString)")
10+
return url
11+
}
12+
13+
@Test func `load returns empty when file is missing`() {
14+
let tempDir = Self.makeTempDir()
15+
defer { try? FileManager.default.removeItem(at: tempDir) }
16+
17+
let store = PluginSettingsStore(rootDir: tempDir)
18+
#expect(store.load(pluginId: "asc-pro.ai").isEmpty)
19+
}
20+
21+
@Test func `save and load round-trips JSON`() throws {
22+
let tempDir = Self.makeTempDir()
23+
defer { try? FileManager.default.removeItem(at: tempDir) }
24+
25+
let store = PluginSettingsStore(rootDir: tempDir)
26+
try store.save(pluginId: "asc-pro.ai", value: [
27+
"apiKey": "abc",
28+
"model": "gpt-5",
29+
])
30+
31+
let loaded = store.load(pluginId: "asc-pro.ai")
32+
#expect(loaded["apiKey"] as? String == "abc")
33+
#expect(loaded["model"] as? String == "gpt-5")
34+
}
35+
36+
@Test func `save overwrites the previous file`() throws {
37+
let tempDir = Self.makeTempDir()
38+
defer { try? FileManager.default.removeItem(at: tempDir) }
39+
40+
let store = PluginSettingsStore(rootDir: tempDir)
41+
try store.save(pluginId: "asc-pro.ai", value: ["apiKey": "old"])
42+
try store.save(pluginId: "asc-pro.ai", value: ["apiKey": "new"])
43+
44+
let loaded = store.load(pluginId: "asc-pro.ai")
45+
#expect(loaded["apiKey"] as? String == "new")
46+
}
47+
48+
@Test func `load returns empty on malformed JSON`() throws {
49+
let tempDir = Self.makeTempDir()
50+
defer { try? FileManager.default.removeItem(at: tempDir) }
51+
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
52+
try "not valid json".write(
53+
to: tempDir.appendingPathComponent("asc-pro.ai.json"),
54+
atomically: true,
55+
encoding: .utf8,
56+
)
57+
58+
let store = PluginSettingsStore(rootDir: tempDir)
59+
#expect(store.load(pluginId: "asc-pro.ai").isEmpty)
60+
}
61+
62+
@Test func `sanitises plugin id to prevent path traversal`() {
63+
let tempDir = Self.makeTempDir()
64+
defer { try? FileManager.default.removeItem(at: tempDir) }
65+
66+
let store = PluginSettingsStore(rootDir: tempDir)
67+
// ".." / "/" characters must be scrubbed so the resolved file URL
68+
// stays under `rootDir` — otherwise a malicious plugin could write
69+
// to arbitrary user files.
70+
let url = store.fileURL(for: "../malicious")
71+
#expect(url.path.hasPrefix(tempDir.path),
72+
"file URL escapes the root directory: \(url.path)")
73+
}
74+
75+
@Test func `nested plugin ids are sanitised`() {
76+
let tempDir = Self.makeTempDir()
77+
defer { try? FileManager.default.removeItem(at: tempDir) }
78+
79+
let store = PluginSettingsStore(rootDir: tempDir)
80+
let url = store.fileURL(for: "asc-pro/ai")
81+
#expect(!url.path.contains("/asc-pro/ai"),
82+
"slash in plugin id must be replaced; got \(url.path)")
83+
}
84+
}

0 commit comments

Comments
 (0)