1import AppKit2import Foundation3import Testing45@testable import Haven67private final class ManagerURLProtocol: URLProtocol, @unchecked Sendable {8 override class func canInit(with request: URLRequest) -> Bool { true }9 override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }10 override func startLoading() {11 let path = request.url!.path12 let statusCode = path.contains("fail-http") ? 500 : 20013 let data: Data14 if path.contains("image.png") {15 data = onePixelPNG16 } else if path.contains("invalid.png") {17 data = Data("not an image".utf8)18 } else {19 data = Data("wallpaper".utf8)20 }21 let response = HTTPURLResponse(22 url: request.url!, statusCode: statusCode, httpVersion: nil, headerFields: nil)!23 client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)24 client?.urlProtocol(self, didLoad: data)25 client?.urlProtocolDidFinishLoading(self)26 }27 override func stopLoading() {}28}2930private func managerSession() -> URLSession {31 let configuration = URLSessionConfiguration.ephemeral32 configuration.protocolClasses = [ManagerURLProtocol.self]33 return URLSession(configuration: configuration)34}3536private let onePixelPNG = Data(37 base64Encoded:38 "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="39)!4041@MainActor42private final class DesktopImageStub: DesktopImageSetting {43 var receivedURLs: [URL] = []4445 func setDesktopImageURL(46 _ url: URL, for screen: NSScreen, options: [NSWorkspace.DesktopImageOptionKey: Any]47 ) throws {48 receivedURLs.append(url)49 }50}5152@Suite("Favorites persistence", .serialized)53struct FavoritesManagerTests {54 private let defaultsKey = "favoriteWallpapers"5556 @Test("Toggling adds and removes a favorite")57 func toggleFavorite() {58 UserDefaults.standard.removeObject(forKey: defaultsKey)59 defer { UserDefaults.standard.removeObject(forKey: defaultsKey) }60 let manager = FavoritesManager()61 let wallpaper = makeWallpaper()6263 manager.toggleFavorite(wallpaper)64 #expect(manager.favorites == [wallpaper])65 #expect(manager.isFavorite(wallpaper))6667 manager.toggleFavorite(wallpaper)68 #expect(manager.favorites.isEmpty)69 #expect(!manager.isFavorite(wallpaper))70 }7172 @Test("Favorites survive manager recreation")73 func favoritesPersist() {74 UserDefaults.standard.removeObject(forKey: defaultsKey)75 defer { UserDefaults.standard.removeObject(forKey: defaultsKey) }76 let wallpaper = makeWallpaper()77 FavoritesManager().toggleFavorite(wallpaper)7879 let reloaded = FavoritesManager()8081 #expect(reloaded.favorites == [wallpaper])82 }83}8485@Suite("History persistence", .serialized)86struct HistoryManagerTests {87 private let defaultsKey = "wallpaperHistory"8889 @Test("History keeps newest items first and promotes duplicates")90 func orderingAndPromotion() {91 UserDefaults.standard.removeObject(forKey: defaultsKey)92 defer { UserDefaults.standard.removeObject(forKey: defaultsKey) }93 let manager = HistoryManager()94 let first = makeWallpaper(id: "first")95 let second = makeWallpaper(id: "second")9697 manager.addToHistory(first)98 manager.addToHistory(second)99 manager.addToHistory(first)100101 #expect(manager.history.map(\.id) == ["first", "second"])102 }103104 @Test("History is capped at fifty entries")105 func historyLimit() {106 UserDefaults.standard.removeObject(forKey: defaultsKey)107 defer { UserDefaults.standard.removeObject(forKey: defaultsKey) }108 let manager = HistoryManager()109110 for index in 0..<55 {111 manager.addToHistory(makeWallpaper(id: "item-\(index)"))112 }113114 #expect(manager.history.count == 50)115 #expect(manager.history.first?.id == "item-54")116 #expect(manager.history.last?.id == "item-5")117 }118119 @Test("History survives recreation and clear removes persisted data")120 func persistenceAndClear() {121 UserDefaults.standard.removeObject(forKey: defaultsKey)122 defer { UserDefaults.standard.removeObject(forKey: defaultsKey) }123 HistoryManager().addToHistory(makeWallpaper(id: "persisted"))124125 let reloaded = HistoryManager()126 #expect(reloaded.history.map(\.id) == ["persisted"])127128 reloaded.clearHistory()129 #expect(HistoryManager().history.isEmpty)130 }131}132133@Suite("Image cache")134struct ImageCacheTests {135 @Test("Inserted images are returned for the same URL")136 func insertAndRetrieve() {137 let url = URL(string: "https://example.com/\(UUID().uuidString).png")!138 let image = NSImage(size: NSSize(width: 10, height: 20))139140 ImageCache.shared.insertImage(image, for: url)141142 #expect(ImageCache.shared.image(for: url) === image)143 }144145 @Test("Different URLs do not collide")146 func keysDoNotCollide() {147 let base = UUID().uuidString148 let firstURL = URL(string: "https://example.com/\(base)-first.png")!149 let secondURL = URL(string: "https://example.com/\(base)-second.png")!150151 ImageCache.shared.insertImage(NSImage(size: NSSize(width: 1, height: 1)), for: firstURL)152153 #expect(ImageCache.shared.image(for: secondURL) == nil)154 }155156 @Test("Fetch decodes and caches successful image responses")157 func fetchAndCache() async throws {158 let cache = ImageCache(session: managerSession())159 let url = URL(string: "https://example.com/image.png")!160161 let first = try await cache.fetchImage(url: url)162 let second = try await cache.fetchImage(url: url)163164 #expect(first === second)165 }166167 @Test("Fetch rejects HTTP errors and invalid image bytes")168 func fetchFailures() async {169 let cache = ImageCache(session: managerSession())170 let httpError = URL(string: "https://example.com/fail-http.png")!171 let invalid = URL(string: "https://example.com/invalid.png")!172 await #expect(throws: WallhavenError.self) { try await cache.fetchImage(url: httpError) }173 await #expect(throws: WallhavenError.self) { try await cache.fetchImage(url: invalid) }174 }175}176177@Suite("Wallpaper file management", .serialized)178@MainActor179struct WallpaperManagerTests {180 @Test("Download validates HTTP status and writes response data")181 func download() async throws {182 let manager = WallpaperManager(session: managerSession())183 let url = URL(string: "https://example.com/\(UUID().uuidString).jpg")!184 let downloaded = try await manager.download(from: url)185 defer { try? FileManager.default.removeItem(at: downloaded) }186187 #expect(try Data(contentsOf: downloaded) == Data("wallpaper".utf8))188189 let errorURL = URL(string: "https://example.com/fail-http.jpg")!190 await #expect(throws: WallhavenError.self) { try await manager.download(from: errorURL) }191 }192193 @Test("Desktop setter receives explicit and default screens")194 @MainActor195 func desktopSetter() throws {196 guard let screen = NSScreen.screens.first else { return }197 var received: [NSScreen] = []198 let manager = WallpaperManager(desktopSetter: { _, target in received.append(target) })199 let url = URL(fileURLWithPath: "/tmp/test-wallpaper.jpg")200201 try manager.setDesktopImage(from: url, screen: screen)202 try manager.setDesktopImage(from: url)203204 #expect(received.count == 2)205 #expect(received[0] === screen)206 }207208 @Test("Default desktop adapter forwards the image to its workspace")209 func defaultDesktopAdapter() throws {210 guard let screen = NSScreen.screens.first else { return }211 let workspace = DesktopImageStub()212 let manager = WallpaperManager(workspace: workspace)213 let url = URL(fileURLWithPath: "/tmp/test-wallpaper.jpg")214215 try manager.setDesktopImage(from: url, screen: screen)216217 #expect(workspace.receivedURLs == [url])218 }219220 @Test("Set wallpaper composes download and desktop update")221 @MainActor222 func setWallpaper() async throws {223 guard let screen = NSScreen.screens.first else { return }224 var receivedURL: URL?225 let manager = WallpaperManager(226 session: managerSession(), desktopSetter: { url, _ in receivedURL = url })227 let remote = URL(string: "https://example.com/\(UUID().uuidString).jpg")!228229 try await manager.setWallpaper(from: remote, screen: screen)230 defer { if let receivedURL { try? FileManager.default.removeItem(at: receivedURL) } }231232 233 234 #expect(receivedURL?.pathExtension == remote.pathExtension)235 #expect(receivedURL?.lastPathComponent.hasPrefix("haven-") == true)236 #expect(receivedURL?.lastPathComponent != remote.lastPathComponent)237 #expect(try Data(contentsOf: receivedURL!) == Data("wallpaper".utf8))238 }239240 @Test("Saving creates the destination folder and copies the file")241 func saveCreatesFolderAndCopies() throws {242 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)243 defer { try? FileManager.default.removeItem(at: root) }244 let source = root.appendingPathComponent("source/wallpaper.jpg")245 let destination = root.appendingPathComponent("nested/destination")246 try FileManager.default.createDirectory(247 at: source.deletingLastPathComponent(), withIntermediateDirectories: true)248 try Data("first".utf8).write(to: source)249250 try WallpaperManager.shared.saveToFolder(from: source, to: destination)251252 let copied = try Data(contentsOf: destination.appendingPathComponent("wallpaper.jpg"))253 #expect(copied == Data("first".utf8))254 }255256 @Test("Saving replaces an existing file with fresh data")257 func saveReplacesExistingFile() throws {258 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)259 defer { try? FileManager.default.removeItem(at: root) }260 let source = root.appendingPathComponent("source/wallpaper.jpg")261 let destination = root.appendingPathComponent("destination")262 try FileManager.default.createDirectory(263 at: source.deletingLastPathComponent(), withIntermediateDirectories: true)264 try FileManager.default.createDirectory(at: destination, withIntermediateDirectories: true)265 try Data("new".utf8).write(to: source)266 try Data("old".utf8).write(to: destination.appendingPathComponent("wallpaper.jpg"))267268 try WallpaperManager.shared.saveToFolder(from: source, to: destination)269270 let copied = try Data(contentsOf: destination.appendingPathComponent("wallpaper.jpg"))271 #expect(copied == Data("new".utf8))272 }273274 @Test("Saving the same logical wallpaper leaves the existing file untouched")275 func saveSameContentIsIdempotent() throws {276 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)277 defer { try? FileManager.default.removeItem(at: root) }278 let source = root.appendingPathComponent("source/wallpaper.jpg")279 let destination = root.appendingPathComponent("destination")280 try FileManager.default.createDirectory(281 at: source.deletingLastPathComponent(), withIntermediateDirectories: true)282 try FileManager.default.createDirectory(at: destination, withIntermediateDirectories: true)283 try Data("identical".utf8).write(to: source)284 let existingURL = destination.appendingPathComponent("wallpaper.jpg")285 try Data("identical".utf8).write(to: existingURL)286 let originalAttributes = try FileManager.default.attributesOfItem(atPath: existingURL.path)287288 try WallpaperManager.shared.saveToFolder(from: source, to: destination)289290 let updatedAttributes = try FileManager.default.attributesOfItem(atPath: existingURL.path)291 #expect(try Data(contentsOf: existingURL) == Data("identical".utf8))292 #expect(293 (updatedAttributes[.modificationDate] as? Date)294 == (originalAttributes[.modificationDate] as? Date)295 )296 }297298 @Test("Saving into the same path as the source does not destroy data")299 func saveSamePathAsSource() throws {300 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)301 defer { try? FileManager.default.removeItem(at: root) }302 try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)303 let source = root.appendingPathComponent("wallpaper.jpg")304 try Data("payload".utf8).write(to: source)305306 try WallpaperManager.shared.saveToFolder(from: source, to: root)307308 #expect(try Data(contentsOf: source) == Data("payload".utf8))309 }310311 @Test("Distinct URLs sharing a lastPathComponent produce distinct temp files")312 func distinctDownloadsDoNotCollide() async throws {313 let manager = WallpaperManager(session: managerSession())314 let first = URL(string: "https://example.com/aaa/wallpaper.jpg")!315 let second = URL(string: "https://example.com/bbb/wallpaper.jpg")!316317 let firstFile = try await manager.download(from: first)318 let secondFile = try await manager.download(from: second)319 defer {320 try? FileManager.default.removeItem(at: firstFile)321 try? FileManager.default.removeItem(at: secondFile)322 }323324 #expect(firstFile.path != secondFile.path)325 #expect(firstFile.lastPathComponent != secondFile.lastPathComponent)326 #expect(try Data(contentsOf: firstFile) == Data("wallpaper".utf8))327 #expect(try Data(contentsOf: secondFile) == Data("wallpaper".utf8))328 }329330 @Test("Repeated downloads of the same URL produce independent temp files")331 func repeatedDownloadsAreIndependent() async throws {332 let manager = WallpaperManager(session: managerSession())333 let remote = URL(string: "https://example.com/\(UUID().uuidString).jpg")!334335 let first = try await manager.download(from: remote)336 let second = try await manager.download(from: remote)337 defer {338 try? FileManager.default.removeItem(at: first)339 try? FileManager.default.removeItem(at: second)340 }341342 #expect(first.path != second.path)343 #expect(try Data(contentsOf: first) == Data("wallpaper".utf8))344 #expect(try Data(contentsOf: second) == Data("wallpaper".utf8))345 }346347 @Test("makeTempURL preserves the source extension when present")348 func makeTempURLPreservesExtension() {349 let withExtension = URL(string: "https://example.com/foo/wallpaper.png")!350 let withoutExtension = URL(string: "https://example.com/foo/wallpaper")!351352 #expect(WallpaperManager.makeTempURL(for: withExtension).pathExtension == "png")353 #expect(WallpaperManager.makeTempURL(for: withoutExtension).pathExtension == "img")354 #expect(355 WallpaperManager.makeTempURL(for: withExtension).path356 != WallpaperManager.makeTempURL(for: withExtension).path)357 }358}359360@Suite("Autostart persistence", .serialized)361@MainActor362struct AutostartManagerTests {363 @Test("Enable writes a launch agent and disable removes it")364 func enableAndDisable() throws {365 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)366 defer { try? FileManager.default.removeItem(at: root) }367 let manager = AutostartManager(368 libraryDirectory: root, executablePath: { "/Applications/Haven.app/Contents/MacOS/Haven" })369370 #expect(!manager.isEnabled)371 manager.isEnabled = true372 #expect(manager.isEnabled)373 manager.isEnabled = true374375 let plist = root.appendingPathComponent("LaunchAgents/com.june.Haven.plist")376 let contents =377 try PropertyListSerialization.propertyList(378 from: Data(contentsOf: plist), format: nil) as? [String: Any]379 #expect(contents?["Label"] as? String == "com.june.Haven")380 #expect(381 contents?["ProgramArguments"] as? [String]382 == ["/Applications/Haven.app/Contents/MacOS/Haven"])383 #expect(contents?["RunAtLoad"] as? Bool == true)384385 manager.isEnabled = false386 #expect(!manager.isEnabled)387 manager.isEnabled = false388 }389390 @Test("Missing executable leaves autostart disabled")391 func missingExecutable() {392 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)393 defer { try? FileManager.default.removeItem(at: root) }394 let manager = AutostartManager(libraryDirectory: root, executablePath: { nil })395396 manager.isEnabled = true397398 #expect(!manager.isEnabled)399 }400401 @Test("Default executable provider writes only inside the injected library")402 func defaultExecutableProvider() {403 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)404 defer { try? FileManager.default.removeItem(at: root) }405 let manager = AutostartManager(libraryDirectory: root)406 manager.isEnabled = true407 #expect(manager.isEnabled)408 manager.isEnabled = false409 }410411 @Test("Unwritable launch-agent roots do not enable autostart")412 func writeFailure() throws {413 let file = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)414 defer { try? FileManager.default.removeItem(at: file) }415 try Data("not a directory".utf8).write(to: file)416 let manager = AutostartManager(libraryDirectory: file, executablePath: { "/tmp/Haven" })417 manager.isEnabled = true418 #expect(!manager.isEnabled)419 }420421 @Test("Removal failures leave the launch agent enabled")422 func removalFailure() {423 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)424 defer { try? FileManager.default.removeItem(at: root) }425 let manager = AutostartManager(426 libraryDirectory: root, executablePath: { "/tmp/Haven" },427 removePlist: { _ in throw CocoaError(.fileWriteNoPermission) })428 manager.isEnabled = true429 manager.isEnabled = false430 #expect(manager.isEnabled)431 }432}433434@Suite("Local wallpaper library", .serialized)435@MainActor436struct LocalWallpapersManagerTests {437 private func waitForScan(_ manager: LocalWallpapersManager) async {438 for _ in 0..<100 where manager.isLoading {439 try? await Task.sleep(nanoseconds: 10_000_000)440 }441 }442443 @Test("Scan filters files, reads metadata, and deletes wallpapers")444 func scanAndDelete() async throws {445 let defaults = UserDefaults.standard446 let oldPath = defaults.string(forKey: "saveFolderPath")447 defaults.removeObject(forKey: "saveFolderPath")448 defer {449 if let oldPath {450 defaults.set(oldPath, forKey: "saveFolderPath")451 } else {452 defaults.removeObject(forKey: "saveFolderPath")453 }454 }455 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)456 defer { try? FileManager.default.removeItem(at: root) }457 try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)458 try onePixelPNG.write(to: root.appendingPathComponent("wallpaper.png"))459 try Data("broken".utf8).write(to: root.appendingPathComponent("broken.png"))460 try Data("ignored".utf8).write(to: root.appendingPathComponent("notes.txt"))461 let manager = LocalWallpapersManager()462463 manager.scanFolder(path: root.path)464 manager.scanFolder(path: root.path)465 await waitForScan(manager)466467 let wallpaper = try #require(manager.wallpapers.first)468 #expect(manager.wallpapers.count == 1)469 #expect(wallpaper.thumbnailURL == wallpaper.url)470 #expect(wallpaper.filename == "wallpaper.png")471 #expect(wallpaper.resolution == "1x1")472 #expect(LocalWallpapersManager.wallpaper(at: root.appendingPathComponent("broken.png")) == nil)473 #expect(LocalWallpapersManager.wallpaper(at: root.appendingPathComponent("missing.png")) == nil)474475 manager.deleteWallpaper(wallpaper)476 #expect(manager.wallpapers.isEmpty)477 manager.deleteWallpaper(wallpaper)478 #expect(manager.errorMessage?.hasPrefix("Failed to delete") == true)479 }480481 @Test("Invalid scan roots surface an error")482 func scanError() async throws {483 let defaults = UserDefaults.standard484 defaults.removeObject(forKey: "saveFolderPath")485 let file = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)486 defer { try? FileManager.default.removeItem(at: file) }487 try Data("file".utf8).write(to: file)488 let manager = LocalWallpapersManager()489490 manager.scanFolder(path: file.path)491 await waitForScan(manager)492493 #expect(manager.errorMessage?.hasPrefix("Failed to scan folder") == true)494 #expect(!manager.isLoading)495 }496497 @Test("Monitoring configured folders observes filesystem changes and stops")498 func monitoring() async throws {499 let defaults = UserDefaults.standard500 let oldPath = defaults.string(forKey: "saveFolderPath")501 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)502 try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)503 defaults.set(root.path, forKey: "saveFolderPath")504 defer {505 try? FileManager.default.removeItem(at: root)506 if let oldPath {507 defaults.set(oldPath, forKey: "saveFolderPath")508 } else {509 defaults.removeObject(forKey: "saveFolderPath")510 }511 }512 let manager = LocalWallpapersManager()513 await waitForScan(manager)514515 try onePixelPNG.write(to: root.appendingPathComponent("observed.png"))516 try? await Task.sleep(nanoseconds: 100_000_000)517 await waitForScan(manager)518 #expect(manager.wallpapers.contains(where: { $0.filename == "observed.png" }))519520 manager.stopMonitoring()521 try? await Task.sleep(nanoseconds: 500_000_000)522523 let invalidMonitor = FolderMonitor(folderPath: root.appendingPathComponent("missing").path) {}524 invalidMonitor.start()525 invalidMonitor.closeFileDescriptor()526527 let descriptor = open("/dev/null", O_RDONLY)528 let descriptorMonitor = FolderMonitor(529 folderPath: "/dev/null", fileDescriptor: descriptor, callback: {})530 descriptorMonitor.closeFileDescriptor()531 #expect(fcntl(descriptor, F_GETFD) == -1)532 }533}