import Foundation import Testing import ShotdeckCore @Test func transportDefaultsToAirDropWhenUnset() throws { let suite = try makeTransportDefaultsSuite() defer { tearDownTransportSuite(suite) } #expect(TransportSettings.transport(defaults: suite.defaults) == .airDrop) } @Test func setTransportRoundTrips() throws { let suite = try makeTransportDefaultsSuite() defer { tearDownTransportSuite(suite) } TransportSettings.setTransport(.oneDrive, defaults: suite.defaults) #expect(TransportSettings.transport(defaults: suite.defaults) == .oneDrive) TransportSettings.setTransport(.airDrop, defaults: suite.defaults) #expect(TransportSettings.transport(defaults: suite.defaults) == .airDrop) } @Test func garbageStoredTransportFallsBackToAirDrop() throws { let suite = try makeTransportDefaultsSuite() defer { tearDownTransportSuite(suite) } suite.defaults.set("not-a-real-transport", forKey: TransportSettings.transportDefaultsKey) #expect(TransportSettings.transport(defaults: suite.defaults) == .airDrop) } @Test func oneDriveFolderStoreAndReset() throws { let suite = try makeTransportDefaultsSuite() defer { tearDownTransportSuite(suite) } let folder = try makeTransportTemporaryDirectory(prefix: "shotdeck-onedrive-folder") defer { try? FileManager.default.removeItem(at: folder) } #expect(TransportSettings.storedOneDriveFolderPath(defaults: suite.defaults) == nil) TransportSettings.setOneDriveFolder(folder, defaults: suite.defaults) #expect(TransportSettings.storedOneDriveFolderPath(defaults: suite.defaults) == folder.path) TransportSettings.resetOneDriveFolder(defaults: suite.defaults) #expect(TransportSettings.storedOneDriveFolderPath(defaults: suite.defaults) == nil) } @Test func effectiveFoldersForAirDropMatchesFolderSettings() throws { let suite = try makeTransportDefaultsSuite() defer { tearDownTransportSuite(suite) } let outbox = try makeTransportTemporaryDirectory(prefix: "shotdeck-effective-outbox") defer { try? FileManager.default.removeItem(at: outbox) } FolderSettings.setOutbox(outbox, defaults: suite.defaults) let effective = TransportSettings.effectiveFolders(defaults: suite.defaults) let expected = FolderSettings.resolve(defaults: suite.defaults) #expect(effective.transport == .airDrop) #expect(effective.outbox.path == expected.outbox.path) #expect(effective.watch.path == expected.watch.path) } @Test func effectiveFoldersForOneDriveWithAResolvableFolderUsesItForBoth() throws { let suite = try makeTransportDefaultsSuite() defer { tearDownTransportSuite(suite) } let folder = try makeTransportTemporaryDirectory(prefix: "shotdeck-effective-onedrive") defer { try? FileManager.default.removeItem(at: folder) } TransportSettings.setTransport(.oneDrive, defaults: suite.defaults) TransportSettings.setOneDriveFolder(folder, defaults: suite.defaults) let effective = TransportSettings.effectiveFolders(defaults: suite.defaults) #expect(effective.transport == .oneDrive) #expect(effective.outbox.path == folder.path) #expect(effective.watch.path == folder.path) #expect(effective.outbox.path == effective.watch.path) } @Test func oneDriveFolderUnavailableErrorDescriptionContainsThePath() throws { let path = "/Users/example/Library/CloudStorage/OneDrive-Example/Redline" let error = ShotdeckError.oneDriveFolderUnavailable(path: path) let description = try #require(error.errorDescription) #expect(!description.isEmpty) #expect(description.contains(path)) } @Test func isWritableDirectoryTrueForAnOrdinaryWritableDirectory() throws { let dir = try makeTransportTemporaryDirectory(prefix: "shotdeck-writable") defer { try? FileManager.default.removeItem(at: dir) } #expect(OneDriveLocator.isWritableDirectory(at: dir)) } @Test func isWritableDirectoryFalseForAnExistingButUnwritableDirectory() throws { let dir = try makeTransportTemporaryDirectory(prefix: "shotdeck-unwritable") defer { // Restore perms BEFORE removal — an unwritable dir can't otherwise be cleaned up. try? FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: dir.path) try? FileManager.default.removeItem(at: dir) } #expect(OneDriveLocator.isWritableDirectory(at: dir)) // sanity check before chmod try FileManager.default.setAttributes([.posixPermissions: 0o500], ofItemAtPath: dir.path) #expect(!OneDriveLocator.isWritableDirectory(at: dir)) } @Test func isWritableDirectoryFalseForAPlainFileAndForANonexistentPath() throws { let dir = try makeTransportTemporaryDirectory(prefix: "shotdeck-writable-check-parent") defer { try? FileManager.default.removeItem(at: dir) } let filePath = dir.appendingPathComponent("plain-file.txt") FileManager.default.createFile(atPath: filePath.path, contents: Data("x".utf8)) #expect(!OneDriveLocator.isWritableDirectory(at: filePath)) #expect(!OneDriveLocator.isWritableDirectory(at: dir.appendingPathComponent("does-not-exist"))) } @Test func probeWritableTrueForAnOrdinaryWritableDirectoryAndLeavesNoProbeFileBehind() throws { let dir = try makeTransportTemporaryDirectory(prefix: "shotdeck-probe-writable") defer { try? FileManager.default.removeItem(at: dir) } #expect(OneDriveLocator.probeWritable(at: dir)) let leftovers = try FileManager.default.contentsOfDirectory(atPath: dir.path) #expect(leftovers.isEmpty) } @Test func probeWritableFalseForAChmod500Directory() throws { // The File Provider edge case this probe exists for: isWritableDirectory can be // true (as verified by the isWritableDirectory tests above) while an actual write // still fails. A chmod 500 directory reproduces that "looks writable, isn't" // shape closely enough to prove the probe itself does a real write, not just // another permissions-bit check. let dir = try makeTransportTemporaryDirectory(prefix: "shotdeck-probe-unwritable") defer { try? FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: dir.path) try? FileManager.default.removeItem(at: dir) } try FileManager.default.setAttributes([.posixPermissions: 0o500], ofItemAtPath: dir.path) #expect(!OneDriveLocator.probeWritable(at: dir)) } @Test func probeWritableFalseForAPlainFilePath() throws { let dir = try makeTransportTemporaryDirectory(prefix: "shotdeck-probe-file-parent") defer { try? FileManager.default.removeItem(at: dir) } let filePath = dir.appendingPathComponent("plain-file.txt") FileManager.default.createFile(atPath: filePath.path, contents: Data("x".utf8)) #expect(!OneDriveLocator.probeWritable(at: filePath)) } /// The write itself succeeds (a real probe file lands on disk via AtomicFile.write, /// which never touches this injected FileManager — it uses raw POSIX calls), but the /// FIRST call to `removeItem(at:)` throws, simulating a transient File Provider /// removal failure. probeWritable's own defer-based cleanup must retry and succeed /// (the second call through this same override falls through to `super`), so no /// probe file is left behind even though the function correctly still reports false /// (the removal it explicitly attempted did fail). private final class ThrowOnceOnRemoveFileManager: FileManager, @unchecked Sendable { private let lock = NSLock() private var hasThrown = false override func removeItem(at URL: URL) throws { lock.lock() let shouldThrow = !hasThrown hasThrown = true lock.unlock() if shouldThrow { throw NSError(domain: "ShotdeckCoreTests.ThrowOnceOnRemove", code: 1) } try super.removeItem(at: URL) } } @Test func probeWritableFalseAndLeavesNoProbeFileWhenRemoveItemThrowsOnce() throws { // Directory fsync failure (the OTHER way probeWritable's cleanup can be needed) has // no injectable seam: AtomicFile.write's directory fsync is a raw Darwin fsync(2) // call on an already-open file descriptor, not parameterized by any FileManager or // other dependency this test can substitute, and there is no portable way to make // fsync(2) itself fail via chmod or other standard test techniques (fsync failures // are OS/filesystem/hardware-level events). Covering the removeItem-throws path // (below) is what this test does; the fsync-throws path is covered by code // inspection only — the same `defer` block guards both. let dir = try makeTransportTemporaryDirectory(prefix: "shotdeck-probe-remove-throws") defer { try? FileManager.default.removeItem(at: dir) } let injectedFileManager = ThrowOnceOnRemoveFileManager() #expect(!OneDriveLocator.probeWritable(at: dir, fileManager: injectedFileManager)) let leftovers = try FileManager.default.contentsOfDirectory(atPath: dir.path) #expect(leftovers.isEmpty) } struct TransportDefaultsSuite { let name: String let defaults: UserDefaults } func makeTransportDefaultsSuite() throws -> TransportDefaultsSuite { let name = "shotdeck-transport-test-\(UUID().uuidString)" let defaults = try #require(UserDefaults(suiteName: name)) defaults.removePersistentDomain(forName: name) return TransportDefaultsSuite(name: name, defaults: defaults) } func tearDownTransportSuite(_ suite: TransportDefaultsSuite) { suite.defaults.removePersistentDomain(forName: suite.name) } func makeTransportTemporaryDirectory(prefix: String) throws -> URL { let url = FileManager.default.temporaryDirectory .appendingPathComponent("\(prefix)-\(UUID().uuidString)", isDirectory: true) try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) return url }