548 lines
20 KiB
Swift
548 lines
20 KiB
Swift
import CryptoKit
|
|
import XCTest
|
|
import Foundation
|
|
import AppKit
|
|
@testable import ClipBored
|
|
|
|
final class ClipboardStoreTests: XCTestCase {
|
|
private var defaults: UserDefaults!
|
|
private var defaultsSuiteName: String!
|
|
private var cacheService: ClipboardCacheService!
|
|
private var baseURL: URL!
|
|
|
|
override func setUpWithError() throws {
|
|
try super.setUpWithError()
|
|
defaultsSuiteName = "com.clipbored.teststore.\(UUID().uuidString)"
|
|
defaults = UserDefaults(suiteName: defaultsSuiteName)
|
|
defaults.removePersistentDomain(forName: defaultsSuiteName)
|
|
baseURL = FileManager.default.temporaryDirectory
|
|
.appendingPathComponent("clipboredtests", isDirectory: true)
|
|
.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
|
try FileManager.default.createDirectory(at: baseURL, withIntermediateDirectories: true)
|
|
cacheService = ClipboardCacheService(baseURL: baseURL, encryptionService: noOpEncryptionService())
|
|
}
|
|
|
|
override func tearDownWithError() throws {
|
|
defaults.removePersistentDomain(forName: defaultsSuiteName)
|
|
if let baseURL {
|
|
try? FileManager.default.removeItem(at: baseURL)
|
|
}
|
|
cacheService = nil
|
|
defaults = nil
|
|
baseURL = nil
|
|
try super.tearDownWithError()
|
|
}
|
|
|
|
func testUpsertMovesDuplicateToFrontAndPersists() throws {
|
|
let settings = makeSettings(maxHistory: 4)
|
|
let store = makeStore(settings: settings)
|
|
let start = Date()
|
|
|
|
store.upsert(makeItem("one", displayText: "One", created: start))
|
|
store.upsert(makeItem("two", displayText: "Two", created: start.addingTimeInterval(-10)))
|
|
store.upsert(makeItem("three", displayText: "Three", created: start.addingTimeInterval(-20)))
|
|
|
|
store.upsert(makeItem("one", displayText: "One (updated)", created: start.addingTimeInterval(-30)))
|
|
store.flushPersistenceForTesting()
|
|
|
|
XCTAssertEqual(store.items.count, 3)
|
|
XCTAssertEqual(store.items.map(\.payload), ["one", "three", "two"])
|
|
XCTAssertEqual(store.items.first?.useCount, 2)
|
|
|
|
let restored = makeStore(settings: settings)
|
|
restored.flushPersistenceForTesting()
|
|
XCTAssertEqual(restored.items.count, 3)
|
|
XCTAssertEqual(restored.items.first?.payload, "one")
|
|
XCTAssertEqual(restored.items.first?.useCount, 2)
|
|
}
|
|
|
|
func testHistoryLimitIsEnforcedByOverflowPurge() throws {
|
|
let settings = makeSettings(maxHistory: 50)
|
|
let store = makeStore(settings: settings)
|
|
let start = Date()
|
|
|
|
(0...50).forEach { i in
|
|
store.upsert(makeItem("item-\(i)", displayText: "\(i)", created: start.addingTimeInterval(-Double(i))))
|
|
}
|
|
store.flushPersistenceForTesting()
|
|
|
|
XCTAssertEqual(store.items.count, 50)
|
|
XCTAssertEqual(store.items.first?.payload, "item-50")
|
|
XCTAssertEqual(store.items.last?.payload, "item-1")
|
|
|
|
let restored = makeStore(settings: settings)
|
|
restored.flushPersistenceForTesting()
|
|
XCTAssertEqual(restored.items.count, 50)
|
|
XCTAssertFalse(restored.items.contains(where: { $0.payload == "item-0" }))
|
|
XCTAssertTrue(restored.items.contains(where: { $0.payload == "item-50" }))
|
|
XCTAssertTrue(restored.items.contains(where: { $0.payload == "item-1" }))
|
|
}
|
|
|
|
func testMarkUsedUpdatesStateAndWritesMutationOnlyOnce() {
|
|
let settings = makeSettings(maxHistory: 50)
|
|
let store = makeStore(settings: settings)
|
|
|
|
store.upsert(makeItem("same", displayText: "First", created: Date()))
|
|
store.upsert(makeItem("same", displayText: "First duplicate", created: Date().addingTimeInterval(1)))
|
|
store.flushPersistenceForTesting()
|
|
|
|
let first = try! XCTUnwrap(store.items.first)
|
|
let firstID = first.id
|
|
store.markUsed(firstID)
|
|
store.flushPersistenceForTesting()
|
|
|
|
let restored = makeStore(settings: settings)
|
|
restored.flushPersistenceForTesting()
|
|
XCTAssertEqual(restored.items.count, 1)
|
|
XCTAssertEqual(restored.items.first?.useCount, 3)
|
|
XCTAssertEqual(restored.items.first?.displayText, "First duplicate")
|
|
}
|
|
|
|
func testTogglePinPersistsAcrossReload() {
|
|
let settings = makeSettings(maxHistory: 50)
|
|
let store = makeStore(settings: settings)
|
|
|
|
store.upsert(makeItem("alpha", displayText: "A", created: Date()))
|
|
store.flushPersistenceForTesting()
|
|
|
|
let itemID = try! XCTUnwrap(store.items.first?.id)
|
|
store.togglePin(itemID)
|
|
store.flushPersistenceForTesting()
|
|
|
|
let restored = makeStore(settings: settings)
|
|
restored.flushPersistenceForTesting()
|
|
XCTAssertEqual(restored.items.first?.isPinned, true)
|
|
}
|
|
|
|
func testSetCollectionPersistsAcrossReload() {
|
|
let settings = makeSettings(maxHistory: 50)
|
|
let store = makeStore(settings: settings)
|
|
|
|
store.upsert(makeItem("alpha", displayText: "A", created: Date()))
|
|
store.flushPersistenceForTesting()
|
|
|
|
let itemID = try! XCTUnwrap(store.items.first?.id)
|
|
store.setCollection(itemID, name: " Client Work ")
|
|
store.flushPersistenceForTesting()
|
|
|
|
let restored = makeStore(settings: settings)
|
|
restored.flushPersistenceForTesting()
|
|
XCTAssertEqual(restored.items.first?.collectionName, "Client Work")
|
|
|
|
let restoredID = try! XCTUnwrap(restored.items.first?.id)
|
|
restored.setCollection(restoredID, name: nil)
|
|
restored.flushPersistenceForTesting()
|
|
|
|
let cleared = makeStore(settings: settings)
|
|
cleared.flushPersistenceForTesting()
|
|
XCTAssertNil(cleared.items.first?.collectionName)
|
|
}
|
|
|
|
func testLegacyJSONHistoryMigratesToSQLite() throws {
|
|
let settings = makeSettings(maxHistory: 50)
|
|
let itemID = UUID()
|
|
let legacyJSON = """
|
|
[
|
|
{
|
|
"id": "\(itemID.uuidString)",
|
|
"kind": 0,
|
|
"displayText": "Legacy Note",
|
|
"payload": "legacy payload",
|
|
"payloadHash": "legacy-hash",
|
|
"createdAt": "2026-06-27T12:00:00Z",
|
|
"lastUsedAt": "2026-06-27T12:01:00Z",
|
|
"useCount": 3,
|
|
"sourceApp": "Notes",
|
|
"imagePath": null,
|
|
"thumbnailPath": null
|
|
}
|
|
]
|
|
"""
|
|
try legacyJSON.data(using: .utf8)!.write(to: baseURL.appendingPathComponent("history.json"))
|
|
|
|
let store = makeStore(settings: settings)
|
|
store.flushPersistenceForTesting()
|
|
|
|
XCTAssertEqual(store.items.count, 1)
|
|
XCTAssertEqual(store.items.first?.id, itemID)
|
|
XCTAssertEqual(store.items.first?.payload, "legacy payload")
|
|
XCTAssertEqual(store.items.first?.useCount, 3)
|
|
|
|
let restored = makeStore(settings: settings)
|
|
restored.flushPersistenceForTesting()
|
|
XCTAssertEqual(restored.items.first?.payload, "legacy payload")
|
|
}
|
|
|
|
func testPinnedItemsSurviveNormalHistoryPrune() {
|
|
let settings = makeSettings(maxHistory: 50)
|
|
let store = makeStore(settings: settings)
|
|
let start = Date()
|
|
|
|
store.upsert(makeItem("pinned-old", displayText: "Pinned", created: start.addingTimeInterval(-500)))
|
|
let pinnedID = try! XCTUnwrap(store.items.first?.id)
|
|
store.togglePin(pinnedID)
|
|
|
|
(0..<60).forEach { index in
|
|
store.upsert(makeItem("new-\(index)", displayText: "New \(index)", created: start.addingTimeInterval(Double(index))))
|
|
}
|
|
store.flushPersistenceForTesting()
|
|
|
|
XCTAssertTrue(store.items.contains(where: { $0.payload == "pinned-old" && $0.isPinned }))
|
|
XCTAssertEqual(store.items.filter { !$0.isPinned }.count, 50)
|
|
XCTAssertEqual(store.items.filter(\.isPinned).count, 1)
|
|
|
|
let restored = makeStore(settings: settings)
|
|
restored.flushPersistenceForTesting()
|
|
XCTAssertTrue(restored.items.contains(where: { $0.payload == "pinned-old" && $0.isPinned }))
|
|
}
|
|
|
|
func testStorageFilesUsePrivatePermissions() throws {
|
|
let settings = makeSettings(maxHistory: 50)
|
|
let store = makeStore(settings: settings)
|
|
|
|
store.upsert(makeItem("private", displayText: "Private", created: Date()))
|
|
store.flushPersistenceForTesting()
|
|
|
|
XCTAssertEqual(try posixPermissions(baseURL), 0o700)
|
|
XCTAssertEqual(try posixPermissions(baseURL.appendingPathComponent("history.sqlite")), 0o600)
|
|
}
|
|
|
|
func testStorageDirectoryHonorsEnvironmentOverride() throws {
|
|
let overrideURL = baseURL.appendingPathComponent("OverrideStorage", isDirectory: true)
|
|
setenv(AppConfiguration.storageDirectoryOverrideEnvironmentKey, overrideURL.path, 1)
|
|
defer { unsetenv(AppConfiguration.storageDirectoryOverrideEnvironmentKey) }
|
|
|
|
let resolved = ClipboardStore.storageDirectory()
|
|
|
|
XCTAssertEqual(resolved.path, overrideURL.standardizedFileURL.path)
|
|
XCTAssertTrue(FileManager.default.fileExists(atPath: overrideURL.path))
|
|
XCTAssertEqual(try posixPermissions(overrideURL), 0o700)
|
|
}
|
|
|
|
func testRemoveAllResetsEncryptionKeyAfterClearingDatabase() throws {
|
|
let settings = makeSettings(maxHistory: 50)
|
|
var resetCount = 0
|
|
let keyData = Data(repeating: 7, count: 32)
|
|
let encryptionService = ClipboardEncryptionService(
|
|
keyProvider: { SymmetricKey(data: keyData) },
|
|
resetProvider: { resetCount += 1 }
|
|
)
|
|
let store = makeStore(settings: settings, encryptionService: encryptionService)
|
|
|
|
store.upsert(makeItem("first", displayText: "First", created: Date()))
|
|
store.upsert(makeItem("second", displayText: "Second", created: Date()))
|
|
store.flushPersistenceForTesting()
|
|
|
|
let firstID = try XCTUnwrap(store.items.first?.id)
|
|
store.remove(firstID)
|
|
store.flushPersistenceForTesting()
|
|
XCTAssertEqual(resetCount, 0)
|
|
|
|
store.removeAll()
|
|
store.flushPersistenceForTesting()
|
|
|
|
XCTAssertEqual(resetCount, 1)
|
|
XCTAssertTrue(store.items.isEmpty)
|
|
let restored = makeStore(settings: settings, encryptionService: encryptionService)
|
|
restored.flushPersistenceForTesting()
|
|
XCTAssertTrue(restored.items.isEmpty)
|
|
}
|
|
|
|
func testRemoveAllCompactsDatabaseFile() throws {
|
|
let settings = makeSettings(maxHistory: 2000)
|
|
settings.pruneDuplicates = false
|
|
let store = makeStore(settings: settings)
|
|
let payload = String(repeating: "clipbored database compaction payload ", count: 180)
|
|
|
|
for index in 0..<60 {
|
|
store.upsert(makeItem("\(payload)\(index)", displayText: "Large \(index)", created: Date(timeIntervalSince1970: Double(index))))
|
|
}
|
|
store.flushPersistenceForTesting()
|
|
|
|
let dbURL = baseURL.appendingPathComponent("history.sqlite")
|
|
let sizeBeforeClear = try fileSize(dbURL)
|
|
XCTAssertGreaterThan(sizeBeforeClear, 200_000)
|
|
|
|
store.removeAll()
|
|
store.flushPersistenceForTesting()
|
|
|
|
let sizeAfterClear = try fileSize(dbURL)
|
|
XCTAssertLessThan(sizeAfterClear, sizeBeforeClear / 2)
|
|
XCTAssertEqual(try posixPermissions(dbURL), 0o600)
|
|
}
|
|
|
|
func testPersistedTextFieldsAreEncryptedAndReload() throws {
|
|
let settings = makeSettings(maxHistory: 50)
|
|
let encryptionService = fixedEncryptionService()
|
|
let store = makeStore(settings: settings, encryptionService: encryptionService)
|
|
let item = ClipboardItem(
|
|
id: UUID(),
|
|
kind: .text,
|
|
displayText: "Displayed secret \(UUID().uuidString)",
|
|
payload: "Payload secret \(UUID().uuidString)",
|
|
payloadHash: "Hash secret \(UUID().uuidString)",
|
|
createdAt: Date(timeIntervalSince1970: 100),
|
|
lastUsedAt: Date(timeIntervalSince1970: 100),
|
|
useCount: 1,
|
|
sourceApp: "Secret app \(UUID().uuidString)",
|
|
imagePath: nil,
|
|
thumbnailPath: nil,
|
|
isPinned: false,
|
|
sourceAppBundleId: "com.example.secret.\(UUID().uuidString)",
|
|
ocrText: "OCR secret \(UUID().uuidString)",
|
|
collectionName: "Collection secret \(UUID().uuidString)"
|
|
)
|
|
|
|
store.upsert(item)
|
|
store.flushPersistenceForTesting()
|
|
|
|
let rawDatabaseText = try databaseText()
|
|
XCTAssertTrue(rawDatabaseText.contains(ClipboardEncryptionService.marker))
|
|
XCTAssertFalse(rawDatabaseText.contains(item.displayText))
|
|
XCTAssertFalse(rawDatabaseText.contains(item.payload))
|
|
XCTAssertFalse(rawDatabaseText.contains(item.payloadHash))
|
|
XCTAssertFalse(rawDatabaseText.contains(item.sourceApp!))
|
|
XCTAssertFalse(rawDatabaseText.contains(item.sourceAppBundleId!))
|
|
XCTAssertFalse(rawDatabaseText.contains(item.ocrText!))
|
|
XCTAssertFalse(rawDatabaseText.contains(item.collectionName!))
|
|
|
|
let restored = makeStore(settings: settings, encryptionService: encryptionService)
|
|
restored.flushPersistenceForTesting()
|
|
|
|
XCTAssertEqual(restored.items.first?.displayText, item.displayText)
|
|
XCTAssertEqual(restored.items.first?.payload, item.payload)
|
|
XCTAssertEqual(restored.items.first?.payloadHash, item.payloadHash)
|
|
XCTAssertEqual(restored.items.first?.sourceApp, item.sourceApp)
|
|
XCTAssertEqual(restored.items.first?.sourceAppBundleId, item.sourceAppBundleId)
|
|
XCTAssertEqual(restored.items.first?.ocrText, item.ocrText)
|
|
XCTAssertEqual(restored.items.first?.collectionName, item.collectionName)
|
|
}
|
|
|
|
func testPlaintextDatabaseMigratesToEncryptedFieldsOnLoad() throws {
|
|
let settings = makeSettings(maxHistory: 50)
|
|
let plaintextStore = makeStore(settings: settings, encryptionService: noOpEncryptionService())
|
|
let item = ClipboardItem(
|
|
id: UUID(),
|
|
kind: .text,
|
|
displayText: "Legacy display \(UUID().uuidString)",
|
|
payload: "Legacy payload \(UUID().uuidString)",
|
|
payloadHash: "Legacy hash \(UUID().uuidString)",
|
|
createdAt: Date(timeIntervalSince1970: 200),
|
|
lastUsedAt: Date(timeIntervalSince1970: 200),
|
|
useCount: 1,
|
|
sourceApp: "Legacy source \(UUID().uuidString)",
|
|
imagePath: nil,
|
|
thumbnailPath: nil,
|
|
isPinned: false,
|
|
sourceAppBundleId: "com.example.legacy.\(UUID().uuidString)",
|
|
ocrText: "Legacy OCR \(UUID().uuidString)",
|
|
collectionName: "Legacy collection \(UUID().uuidString)"
|
|
)
|
|
|
|
plaintextStore.upsert(item)
|
|
plaintextStore.flushPersistenceForTesting()
|
|
XCTAssertTrue(try databaseText().contains(item.payload))
|
|
|
|
let encryptionService = fixedEncryptionService()
|
|
let restored = makeStore(settings: settings, encryptionService: encryptionService)
|
|
restored.flushPersistenceForTesting()
|
|
|
|
XCTAssertEqual(restored.items.first?.displayText, item.displayText)
|
|
XCTAssertEqual(restored.items.first?.payload, item.payload)
|
|
XCTAssertEqual(restored.items.first?.payloadHash, item.payloadHash)
|
|
XCTAssertEqual(restored.items.first?.sourceApp, item.sourceApp)
|
|
XCTAssertEqual(restored.items.first?.sourceAppBundleId, item.sourceAppBundleId)
|
|
XCTAssertEqual(restored.items.first?.ocrText, item.ocrText)
|
|
XCTAssertEqual(restored.items.first?.collectionName, item.collectionName)
|
|
|
|
let migratedDatabaseText = try databaseText()
|
|
XCTAssertTrue(migratedDatabaseText.contains(ClipboardEncryptionService.marker))
|
|
XCTAssertFalse(migratedDatabaseText.contains(item.displayText))
|
|
XCTAssertFalse(migratedDatabaseText.contains(item.payload))
|
|
XCTAssertFalse(migratedDatabaseText.contains(item.payloadHash))
|
|
XCTAssertFalse(migratedDatabaseText.contains(item.sourceApp!))
|
|
XCTAssertFalse(migratedDatabaseText.contains(item.sourceAppBundleId!))
|
|
XCTAssertFalse(migratedDatabaseText.contains(item.ocrText!))
|
|
XCTAssertFalse(migratedDatabaseText.contains(item.collectionName!))
|
|
}
|
|
|
|
func testDuplicatePDFReplacementRemovesOldAttachment() throws {
|
|
let settings = makeSettings(maxHistory: 50)
|
|
let store = makeStore(settings: settings)
|
|
let hash = "same-pdf"
|
|
let oldPath = try XCTUnwrap(cacheService.cachePDF(Data("old".utf8), id: UUID()))
|
|
let newPath = try XCTUnwrap(cacheService.cachePDF(Data("new".utf8), id: UUID()))
|
|
|
|
store.upsert(makePDFItem(path: oldPath, hash: hash, created: Date(timeIntervalSince1970: 10)))
|
|
store.upsert(makePDFItem(path: newPath, hash: hash, created: Date(timeIntervalSince1970: 20)))
|
|
store.flushPersistenceForTesting()
|
|
cacheService.flushForTesting()
|
|
|
|
XCTAssertEqual(store.items.count, 1)
|
|
XCTAssertEqual(store.items.first?.payload, newPath)
|
|
XCTAssertFalse(FileManager.default.fileExists(atPath: oldPath))
|
|
XCTAssertTrue(FileManager.default.fileExists(atPath: newPath))
|
|
}
|
|
|
|
func testDuplicateReplacementClearsStaleImageSearchMetadata() throws {
|
|
let settings = makeSettings(maxHistory: 50)
|
|
settings.keepFirstImage = false
|
|
let store = makeStore(settings: settings)
|
|
let staleImage = try XCTUnwrap(cacheService.cacheImage(makeImage(color: .systemBlue), id: UUID()))
|
|
let hash = "same-content"
|
|
let imageItem = makeImageItem(
|
|
fullPath: staleImage.full,
|
|
thumbPath: staleImage.thumb,
|
|
hash: hash,
|
|
ocrText: "stale search marker",
|
|
created: Date(timeIntervalSince1970: 10)
|
|
)
|
|
let textItem = ClipboardItem(
|
|
id: UUID(),
|
|
kind: .text,
|
|
displayText: "Replacement text",
|
|
payload: "Replacement text",
|
|
payloadHash: hash,
|
|
createdAt: Date(timeIntervalSince1970: 20),
|
|
lastUsedAt: Date(timeIntervalSince1970: 20),
|
|
useCount: 1,
|
|
sourceApp: "Notes",
|
|
imagePath: nil,
|
|
thumbnailPath: nil,
|
|
isPinned: false,
|
|
sourceAppBundleId: "com.apple.Notes",
|
|
ocrText: nil
|
|
)
|
|
|
|
store.upsert(imageItem)
|
|
store.upsert(textItem)
|
|
store.flushPersistenceForTesting()
|
|
cacheService.flushForTesting()
|
|
|
|
let item = try XCTUnwrap(store.items.first)
|
|
XCTAssertEqual(store.items.count, 1)
|
|
XCTAssertEqual(item.kind, .text)
|
|
XCTAssertEqual(item.payload, textItem.payload)
|
|
XCTAssertNil(item.imagePath)
|
|
XCTAssertNil(item.thumbnailPath)
|
|
XCTAssertNil(item.ocrText)
|
|
XCTAssertFalse(item.searchableText.contains("stale search marker"))
|
|
XCTAssertFalse(FileManager.default.fileExists(atPath: staleImage.full))
|
|
XCTAssertFalse(FileManager.default.fileExists(atPath: staleImage.thumb))
|
|
}
|
|
|
|
private func makeSettings(maxHistory: Int) -> SettingsModel {
|
|
let settings = SettingsModel(defaults: defaults)
|
|
settings.maxHistoryItems = maxHistory
|
|
settings.pruneDuplicates = true
|
|
settings.keepFirstImage = true
|
|
return settings
|
|
}
|
|
|
|
private func makeStore(settings: SettingsModel) -> ClipboardStore {
|
|
makeStore(settings: settings, encryptionService: noOpEncryptionService())
|
|
}
|
|
|
|
private func makeStore(settings: SettingsModel, encryptionService: ClipboardEncryptionService) -> ClipboardStore {
|
|
ClipboardStore(
|
|
settings: settings,
|
|
cacheService: cacheService,
|
|
baseURL: baseURL,
|
|
encryptionService: encryptionService
|
|
)
|
|
}
|
|
|
|
private func makeItem(_ payload: String, displayText: String, created: Date) -> ClipboardItem {
|
|
let hash = settingsHash(for: payload)
|
|
return ClipboardItem(
|
|
id: UUID(),
|
|
kind: .text,
|
|
displayText: displayText,
|
|
payload: payload,
|
|
payloadHash: hash,
|
|
createdAt: created,
|
|
lastUsedAt: created,
|
|
useCount: 1,
|
|
sourceApp: nil,
|
|
imagePath: nil,
|
|
thumbnailPath: nil,
|
|
isPinned: false
|
|
)
|
|
}
|
|
|
|
private func makePDFItem(path: String, hash: String, created: Date) -> ClipboardItem {
|
|
ClipboardItem(
|
|
id: UUID(),
|
|
kind: .pdf,
|
|
displayText: "PDF",
|
|
payload: path,
|
|
payloadHash: hash,
|
|
createdAt: created,
|
|
lastUsedAt: created,
|
|
useCount: 1,
|
|
sourceApp: nil,
|
|
imagePath: nil,
|
|
thumbnailPath: nil,
|
|
isPinned: false
|
|
)
|
|
}
|
|
|
|
private func makeImageItem(fullPath: String, thumbPath: String, hash: String, ocrText: String?, created: Date) -> ClipboardItem {
|
|
ClipboardItem(
|
|
id: UUID(),
|
|
kind: .image,
|
|
displayText: "Image",
|
|
payload: fullPath,
|
|
payloadHash: hash,
|
|
createdAt: created,
|
|
lastUsedAt: created,
|
|
useCount: 1,
|
|
sourceApp: "Preview",
|
|
imagePath: fullPath,
|
|
thumbnailPath: thumbPath,
|
|
isPinned: false,
|
|
sourceAppBundleId: "com.apple.Preview",
|
|
ocrText: ocrText
|
|
)
|
|
}
|
|
|
|
private func makeImage(color: NSColor) -> NSImage {
|
|
let size = NSSize(width: 24, height: 24)
|
|
let image = NSImage(size: size)
|
|
image.lockFocus()
|
|
color.setFill()
|
|
NSRect(origin: .zero, size: size).fill()
|
|
image.unlockFocus()
|
|
return image
|
|
}
|
|
|
|
private func settingsHash(for payload: String) -> String {
|
|
payload
|
|
}
|
|
|
|
private func posixPermissions(_ url: URL) throws -> Int {
|
|
let attributes = try FileManager.default.attributesOfItem(atPath: url.path)
|
|
return try XCTUnwrap(attributes[.posixPermissions] as? Int) & 0o777
|
|
}
|
|
|
|
private func fileSize(_ url: URL) throws -> Int64 {
|
|
let attributes = try FileManager.default.attributesOfItem(atPath: url.path)
|
|
let size = try XCTUnwrap(attributes[.size] as? NSNumber)
|
|
return size.int64Value
|
|
}
|
|
|
|
private func databaseText() throws -> String {
|
|
let data = try Data(contentsOf: baseURL.appendingPathComponent("history.sqlite"))
|
|
return String(decoding: data, as: UTF8.self)
|
|
}
|
|
|
|
private func noOpEncryptionService() -> ClipboardEncryptionService {
|
|
ClipboardEncryptionService(keyProvider: { nil })
|
|
}
|
|
|
|
private func fixedEncryptionService(byte: UInt8 = 7) -> ClipboardEncryptionService {
|
|
let keyData = Data(repeating: byte, count: 32)
|
|
return ClipboardEncryptionService(keyProvider: { SymmetricKey(data: keyData) })
|
|
}
|
|
}
|