diff --git a/Yattee/Localizable.xcstrings b/Yattee/Localizable.xcstrings index b95f1be2..9c8eb914 100644 --- a/Yattee/Localizable.xcstrings +++ b/Yattee/Localizable.xcstrings @@ -15317,26 +15317,6 @@ } } }, - "sources.bandwidth.download %@" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Download: %@" - } - } - } - }, - "sources.bandwidth.upload %@" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Upload: %@" - } - } - } - }, "sources.browsePeerTube" : { "comment" : "Menu item to browse PeerTube instances", "localizations" : { @@ -16318,17 +16298,6 @@ } } }, - "sources.status.connected" : { - "comment" : "Status when connected", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Connected" - } - } - } - }, "sources.status.disabled" : { "comment" : "Status when source is disabled", "localizations" : { @@ -16340,39 +16309,6 @@ } } }, - "sources.status.readOnly" : { - "comment" : "Status when source is read-only", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Read-only access" - } - } - } - }, - "sources.test.success" : { - "comment" : "Test connection success message", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Connected successfully" - } - } - } - }, - "sources.testConnection" : { - "comment" : "Button to test connection", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Test Connection" - } - } - } - }, "sources.testing" : { "comment" : "Message while testing connection", "localizations" : { diff --git a/Yattee/Services/MediaSources/SMBClient.swift b/Yattee/Services/MediaSources/SMBClient.swift index 8455c04f..4f098499 100644 --- a/Yattee/Services/MediaSources/SMBClient.swift +++ b/Yattee/Services/MediaSources/SMBClient.swift @@ -476,51 +476,6 @@ extension SMBClient { } } -// MARK: - Bandwidth Testing - -extension SMBClient { - - /// Tests bandwidth to an SMB server. - /// - /// Note: This is a placeholder implementation. - /// Real bandwidth testing would require file upload/download operations. - /// - /// - Parameters: - /// - source: The media source configuration. - /// - password: The password for authentication. - /// - testFileSizeMB: Size of test file in megabytes. - /// - progressHandler: Optional callback for progress updates. - /// - Returns: BandwidthTestResult with speed measurements (same type as WebDAV). - func testBandwidth( - source: MediaSource, - password: String?, - testFileSizeMB: Int = 5, - progressHandler: (@Sendable (String) -> Void)? = nil - ) async throws -> BandwidthTestResult { - guard source.type == .smb else { - throw MediaSourceError.unknown("Invalid source type for SMB client") - } - - progressHandler?("Connecting...") - - // Validate connection - _ = try await testConnection(source: source, password: password) - - progressHandler?("Complete") - - // TODO: Implement actual bandwidth testing - LoggingService.shared.logMediaSourcesWarning("SMB bandwidth testing not yet implemented") - - return BandwidthTestResult( - hasWriteAccess: false, - uploadSpeed: nil, - downloadSpeed: nil, - testFileSize: 0, - warning: "Bandwidth testing not available for SMB sources" - ) - } -} - // MARK: - URL Extension for Sanitization extension URL { diff --git a/Yattee/Services/MediaSources/WebDAVClient.swift b/Yattee/Services/MediaSources/WebDAVClient.swift index bffc0212..4307aab6 100644 --- a/Yattee/Services/MediaSources/WebDAVClient.swift +++ b/Yattee/Services/MediaSources/WebDAVClient.swift @@ -95,325 +95,6 @@ actor WebDAVClient { return true } - // MARK: - Bandwidth Testing - - /// Tests bandwidth to a WebDAV server with auto-detection of write access. - /// - Parameters: - /// - source: The media source configuration. - /// - password: The password for authentication. - /// - testFileSizeMB: Size of test file in megabytes (default 5 MB). - /// - progressHandler: Optional callback for progress updates (status string). - /// - Returns: BandwidthTestResult with speed measurements. - func testBandwidth( - source: MediaSource, - password: String?, - testFileSizeMB: Int = 20, - progressHandler: (@Sendable (String) -> Void)? = nil - ) async throws -> BandwidthTestResult { - let bandwidthTestSize = Int64(testFileSizeMB) * 1024 * 1024 - guard source.type == .webdav else { - throw MediaSourceError.unknown("Invalid source type for WebDAV client") - } - - // First, verify basic connectivity - progressHandler?("Connecting...") - _ = try await listFiles(at: "/", source: source, password: password) - - // Try write test first - do { - return try await performWriteTest(source: source, password: password, testSize: bandwidthTestSize, progressHandler: progressHandler) - } catch let error as MediaSourceError { - // Check if it's a permission error - fall back to read-only - if case .connectionFailed(let message) = error, - message.contains("403") || message.contains("405") || message.contains("401") { - return try await performReadOnlyTest(source: source, password: password, testSize: bandwidthTestSize, progressHandler: progressHandler) - } - throw error - } catch { - // For other errors, try read-only test - return try await performReadOnlyTest(source: source, password: password, testSize: bandwidthTestSize, progressHandler: progressHandler) - } - } - - /// Performs a write test: upload, download, and delete a test file. - private func performWriteTest( - source: MediaSource, - password: String?, - testSize: Int64, - progressHandler: (@Sendable (String) -> Void)? - ) async throws -> BandwidthTestResult { - let testFileName = ".yattee-bandwidth-test-\(UUID().uuidString).tmp" - - // Find a writable location - try first subfolder (root may not be writable, e.g. Synology shares listing) - let writablePath = try await findWritablePath(source: source, password: password) - let testPath = writablePath + testFileName - - // Generate test data (zeros are fine for bandwidth testing) - let testData = Data(count: Int(testSize)) - - // Upload test - progressHandler?("Uploading...") - let uploadStart = CFAbsoluteTimeGetCurrent() - try await uploadFile(data: testData, to: testPath, source: source, password: password) - let uploadDuration = CFAbsoluteTimeGetCurrent() - uploadStart - let uploadSpeed = Double(testSize) / uploadDuration - - progressHandler?("Downloading...") - - // Download test - let downloadStart = CFAbsoluteTimeGetCurrent() - _ = try await downloadFile(from: testPath, source: source, password: password) - let downloadDuration = CFAbsoluteTimeGetCurrent() - downloadStart - let downloadSpeed = Double(testSize) / downloadDuration - - progressHandler?("Cleaning up...") - - // Delete test file (ignore errors - cleanup is best effort) - try? await deleteFile(at: testPath, source: source, password: password) - - progressHandler?("Complete") - - return BandwidthTestResult( - hasWriteAccess: true, - uploadSpeed: uploadSpeed, - downloadSpeed: downloadSpeed, - testFileSize: testSize, - warning: nil - ) - } - - /// Performs a read-only test by finding and downloading an existing file. - private func performReadOnlyTest( - source: MediaSource, - password: String?, - testSize: Int64, - progressHandler: (@Sendable (String) -> Void)? - ) async throws -> BandwidthTestResult { - progressHandler?("Finding test file...") - - // Find a file to download - guard let testFile = try await findTestFile(source: source, password: password) else { - // Server is empty or has no accessible files - progressHandler?("Complete") - return BandwidthTestResult( - hasWriteAccess: false, - uploadSpeed: nil, - downloadSpeed: nil, - testFileSize: 0, - warning: "No files available for speed test" - ) - } - - progressHandler?("Downloading...") - - // Download the file (or first N MB of it based on test size) - let downloadStart = CFAbsoluteTimeGetCurrent() - let downloadedSize = try await downloadFilePartial( - from: testFile.path, - source: source, - password: password, - maxBytes: testSize - ) - let downloadDuration = CFAbsoluteTimeGetCurrent() - downloadStart - let downloadSpeed = Double(downloadedSize) / downloadDuration - - progressHandler?("Complete") - - return BandwidthTestResult( - hasWriteAccess: false, - uploadSpeed: nil, - downloadSpeed: downloadSpeed, - testFileSize: Int64(downloadedSize), - warning: nil - ) - } - - /// Finds a writable path for the bandwidth test file. - /// Tries root first, then first available subfolder (useful for Synology where root is shares listing). - private func findWritablePath( - source: MediaSource, - password: String? - ) async throws -> String { - // List root to find first subfolder - let rootFiles = try await listFiles(at: "/", source: source, password: password) - - // Try first directory as writable location - if let firstDir = rootFiles.first(where: { $0.isDirectory }) { - return firstDir.path.hasSuffix("/") ? firstDir.path : firstDir.path + "/" - } - - // Fall back to root if no subdirectories - return "/" - } - - /// Finds a suitable file for download testing. - private func findTestFile( - source: MediaSource, - password: String? - ) async throws -> MediaFile? { - return try await findFileRecursive( - in: "/", - source: source, - password: password, - depth: 0, - maxDepth: 2 - ) - } - - /// Recursively searches for a suitable test file. - private func findFileRecursive( - in path: String, - source: MediaSource, - password: String?, - depth: Int, - maxDepth: Int - ) async throws -> MediaFile? { - let files = try await listFiles(at: path, source: source, password: password) - - // First, look for any file with reasonable size (> 100KB) - if let file = files.first(where: { !$0.isDirectory && ($0.size ?? 0) > 100_000 }) { - return file - } - - // If at max depth, just return any file - if depth >= maxDepth { - return files.first(where: { !$0.isDirectory }) - } - - // Otherwise, recurse into directories - for dir in files.filter({ $0.isDirectory }) { - if let file = try? await findFileRecursive( - in: dir.path, - source: source, - password: password, - depth: depth + 1, - maxDepth: maxDepth - ) { - return file - } - } - - return nil - } - - // MARK: - WebDAV Operations for Bandwidth Test - - /// Uploads data to a WebDAV server. - private func uploadFile( - data: Data, - to path: String, - source: MediaSource, - password: String? - ) async throws { - let normalizedPath = path.hasPrefix("/") ? path : "/\(path)" - let requestURL = source.url.appendingPathComponent(normalizedPath) - - var request = URLRequest(url: requestURL) - request.httpMethod = "PUT" - request.httpBody = data - request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type") - request.setValue("\(data.count)", forHTTPHeaderField: "Content-Length") - request.timeoutInterval = 120 // 2 minutes for upload - - if let authHeader = buildAuthHeader(username: source.username, password: password) { - request.setValue(authHeader, forHTTPHeaderField: "Authorization") - } - - let (_, response) = try await session.data(for: request) - - if let httpResponse = response as? HTTPURLResponse { - guard (200...299).contains(httpResponse.statusCode) || httpResponse.statusCode == 201 else { - throw MediaSourceError.connectionFailed("Upload failed: HTTP \(httpResponse.statusCode)") - } - } - } - - /// Downloads a file from a WebDAV server. - private func downloadFile( - from path: String, - source: MediaSource, - password: String? - ) async throws -> Data { - let normalizedPath = path.hasPrefix("/") ? path : "/\(path)" - let requestURL = source.url.appendingPathComponent(normalizedPath) - - var request = URLRequest(url: requestURL) - request.httpMethod = "GET" - request.timeoutInterval = 120 - - if let authHeader = buildAuthHeader(username: source.username, password: password) { - request.setValue(authHeader, forHTTPHeaderField: "Authorization") - } - - let (data, response) = try await session.data(for: request) - - if let httpResponse = response as? HTTPURLResponse { - guard (200...299).contains(httpResponse.statusCode) else { - throw MediaSourceError.connectionFailed("Download failed: HTTP \(httpResponse.statusCode)") - } - } - - return data - } - - /// Downloads up to maxBytes of a file (using Range header if supported). - private func downloadFilePartial( - from path: String, - source: MediaSource, - password: String?, - maxBytes: Int64 - ) async throws -> Int { - let normalizedPath = path.hasPrefix("/") ? path : "/\(path)" - let requestURL = source.url.appendingPathComponent(normalizedPath) - - var request = URLRequest(url: requestURL) - request.httpMethod = "GET" - request.setValue("bytes=0-\(maxBytes - 1)", forHTTPHeaderField: "Range") - request.timeoutInterval = 120 - - if let authHeader = buildAuthHeader(username: source.username, password: password) { - request.setValue(authHeader, forHTTPHeaderField: "Authorization") - } - - let (data, response) = try await session.data(for: request) - - if let httpResponse = response as? HTTPURLResponse { - // Accept 200 (full file) or 206 (partial content) - guard httpResponse.statusCode == 200 || httpResponse.statusCode == 206 else { - throw MediaSourceError.connectionFailed("Download failed: HTTP \(httpResponse.statusCode)") - } - } - - return data.count - } - - /// Deletes a file from a WebDAV server. - private func deleteFile( - at path: String, - source: MediaSource, - password: String? - ) async throws { - let normalizedPath = path.hasPrefix("/") ? path : "/\(path)" - let requestURL = source.url.appendingPathComponent(normalizedPath) - - var request = URLRequest(url: requestURL) - request.httpMethod = "DELETE" - request.timeoutInterval = 30 - - if let authHeader = buildAuthHeader(username: source.username, password: password) { - request.setValue(authHeader, forHTTPHeaderField: "Authorization") - } - - let (_, response) = try await session.data(for: request) - - if let httpResponse = response as? HTTPURLResponse { - // Accept 200, 204 (No Content), or 404 (already gone) - guard (200...299).contains(httpResponse.statusCode) || httpResponse.statusCode == 404 else { - throw MediaSourceError.connectionFailed("Delete failed: HTTP \(httpResponse.statusCode)") - } - } - } - /// Builds authentication headers for a WebDAV request. /// - Parameters: /// - source: The media source. @@ -477,45 +158,6 @@ actor WebDAVClient { } } -// MARK: - Bandwidth Test Result - -/// Result of a bandwidth test on a WebDAV server. -struct BandwidthTestResult: Sendable { - /// Whether the server allows write access (upload/delete). - let hasWriteAccess: Bool - - /// Upload speed in bytes per second (nil if write access unavailable). - let uploadSpeed: Double? - - /// Download speed in bytes per second. - let downloadSpeed: Double? - - /// Size of the test file used (in bytes). - let testFileSize: Int64 - - /// Any warning message (e.g., "Server appears empty, could not test download speed"). - let warning: String? - - /// Formatted upload speed string (e.g., "12.5 MB/s"). - var formattedUploadSpeed: String? { - guard let speed = uploadSpeed else { return nil } - return Self.formatSpeed(speed) - } - - /// Formatted download speed string (e.g., "45.2 MB/s"). - var formattedDownloadSpeed: String? { - guard let speed = downloadSpeed else { return nil } - return Self.formatSpeed(speed) - } - - private static func formatSpeed(_ bytesPerSecond: Double) -> String { - let formatter = ByteCountFormatter() - formatter.countStyle = .file - formatter.allowedUnits = [.useKB, .useMB, .useGB] - return formatter.string(fromByteCount: Int64(bytesPerSecond)) + "/s" - } -} - // MARK: - WebDAV Response Parser /// Parses WebDAV PROPFIND multi-status XML responses. diff --git a/Yattee/Views/Settings/AddSource/AddSourceShared.swift b/Yattee/Views/Settings/AddSource/AddSourceShared.swift index 97d5ece5..dc830913 100644 --- a/Yattee/Views/Settings/AddSource/AddSourceShared.swift +++ b/Yattee/Views/Settings/AddSource/AddSourceShared.swift @@ -11,8 +11,6 @@ import SwiftUI /// Result of a connection test for WebDAV/SMB sources. enum SourceTestResult { - case success - case successWithBandwidth(BandwidthTestResult) case failure(String) } @@ -25,38 +23,6 @@ struct SourceTestResultSection: View { var body: some View { Section { switch result { - case .success: - Label(String(localized: "sources.status.connected"), systemImage: "checkmark.circle.fill") - .foregroundStyle(.green) - - case .successWithBandwidth(let bandwidth): - VStack(alignment: .leading, spacing: 4) { - Label(String(localized: "sources.status.connected"), systemImage: "checkmark.circle.fill") - .foregroundStyle(.green) - if bandwidth.hasWriteAccess { - if let upload = bandwidth.formattedUploadSpeed { - Label(String(localized: "sources.bandwidth.upload \(upload)"), systemImage: "arrow.up.circle") - .font(.subheadline) - .foregroundStyle(.secondary) - } - } - if let download = bandwidth.formattedDownloadSpeed { - Label(String(localized: "sources.bandwidth.download \(download)"), systemImage: "arrow.down.circle") - .font(.subheadline) - .foregroundStyle(.secondary) - } - if !bandwidth.hasWriteAccess { - Label(String(localized: "sources.status.readOnly"), systemImage: "lock.fill") - .font(.subheadline) - .foregroundStyle(.orange) - } - if let warning = bandwidth.warning { - Text(warning) - .font(.caption) - .foregroundStyle(.secondary) - } - } - case .failure(let error): Label(error, systemImage: "xmark.circle.fill") .foregroundStyle(.red) diff --git a/Yattee/Views/Settings/EditSourceView.swift b/Yattee/Views/Settings/EditSourceView.swift index fa42d25f..7086bb31 100644 --- a/Yattee/Views/Settings/EditSourceView.swift +++ b/Yattee/Views/Settings/EditSourceView.swift @@ -59,15 +59,6 @@ private struct EditRemoteServerContent: View { @State private var isLoadingServerInfo = false @State private var serverInfoError: String? - // Connection testing - @State private var isTesting = false - @State private var testResult: RemoteServerTestResult? - - enum RemoteServerTestResult { - case success - case failure(String) - } - init(instance: Instance) { self.instance = instance _name = State(initialValue: instance.name ?? "") @@ -309,30 +300,6 @@ private struct EditRemoteServerContent: View { } } - Section { - Button { - testConnection() - } label: { - if isTesting { - HStack { - ProgressView() - .controlSize(.small) - Text(String(localized: "sources.testing")) - } - } else { - Label(String(localized: "sources.testConnection"), systemImage: "network") - } - } - .disabled(isTesting) - #if os(tvOS) - .buttonStyle(TVSettingsButtonStyle()) - #endif - } - - if let result = testResult { - testResultSection(result) - } - #if os(tvOS) Section { Button { @@ -503,41 +470,6 @@ private struct EditRemoteServerContent: View { appEnvironment?.instancesManager.update(updated) dismiss() } - - private func testConnection() { - guard let appEnvironment else { return } - isTesting = true - testResult = nil - - Task { - do { - _ = try await appEnvironment.contentService.popular(for: instance) - await MainActor.run { - isTesting = false - testResult = .success - } - } catch { - await MainActor.run { - isTesting = false - testResult = .failure(error.localizedDescription) - } - } - } - } - - @ViewBuilder - private func testResultSection(_ result: RemoteServerTestResult) -> some View { - Section { - switch result { - case .success: - Label(String(localized: "sources.test.success"), systemImage: "checkmark.circle.fill") - .foregroundStyle(.green) - case .failure(let error): - Label(error, systemImage: "xmark.circle.fill") - .foregroundStyle(.red) - } - } - } } // MARK: - File Source Content @@ -554,18 +486,9 @@ private struct EditFileSourceContent: View { @State private var password: String @State private var allowInvalidCertificates: Bool @State private var smbProtocolVersion: SMBProtocol = .auto - @State private var isTesting = false - @State private var testResult: TestResult? - @State private var testProgress: String? @State private var hasExistingPassword = false @State private var showingDeleteConfirmation = false - enum TestResult { - case success - case successWithBandwidth(BandwidthTestResult) - case failure(String) - } - init(source: MediaSource) { self.source = source _name = State(initialValue: source.name) @@ -738,30 +661,6 @@ private struct EditFileSourceContent: View { } footer: { Text(String(localized: "sources.footer.allowInvalidCertificates")) } - - Section { - Button { - testConnection() - } label: { - if isTesting { - HStack { - ProgressView() - .controlSize(.small) - Text(testProgress ?? String(localized: "sources.testing")) - } - } else { - Label(String(localized: "sources.testConnection"), systemImage: "speedometer") - } - } - .disabled(isTesting) - #if os(tvOS) - .buttonStyle(TVSettingsButtonStyle()) - #endif - } - - if let result = testResult { - testResultSection(result) - } } #if os(tvOS) @@ -807,90 +706,6 @@ private struct EditFileSourceContent: View { .presentationCompactAdaptation(.sheet) } - @ViewBuilder - private func testResultSection(_ result: TestResult) -> some View { - Section { - switch result { - case .success: - Label(String(localized: "sources.status.connected"), systemImage: "checkmark.circle.fill") - .foregroundStyle(.green) - case .successWithBandwidth(let bandwidth): - VStack(alignment: .leading, spacing: 4) { - Label(String(localized: "sources.status.connected"), systemImage: "checkmark.circle.fill") - .foregroundStyle(.green) - if bandwidth.hasWriteAccess { - if let upload = bandwidth.formattedUploadSpeed { - Label(String(localized: "sources.bandwidth.upload \(upload)"), systemImage: "arrow.up.circle") - .font(.subheadline) - .foregroundStyle(.secondary) - } - } - if let download = bandwidth.formattedDownloadSpeed { - Label(String(localized: "sources.bandwidth.download \(download)"), systemImage: "arrow.down.circle") - .font(.subheadline) - .foregroundStyle(.secondary) - } - if !bandwidth.hasWriteAccess { - Label(String(localized: "sources.status.readOnly"), systemImage: "lock.fill") - .font(.subheadline) - .foregroundStyle(.orange) - } - if let warning = bandwidth.warning { - Text(warning) - .font(.caption) - .foregroundStyle(.secondary) - } - } - case .failure(let error): - Label(error, systemImage: "xmark.circle.fill") - .foregroundStyle(.red) - } - } - } - - private func testConnection() { - guard let appEnvironment else { return } - - isTesting = true - testResult = nil - testProgress = nil - - let testPassword = password.isEmpty - ? appEnvironment.mediaSourcesManager.password(for: source) - : password - - var updatedSource = source - updatedSource.username = username.isEmpty ? nil : username - updatedSource.allowInvalidCertificates = allowInvalidCertificates - - // Use factory to create client with appropriate SSL settings - let webDAVClient = appEnvironment.webDAVClientFactory.createClient(for: updatedSource) - - Task { - do { - let bandwidthResult = try await webDAVClient.testBandwidth( - source: updatedSource, - password: testPassword - ) { status in - Task { @MainActor in - self.testProgress = status - } - } - await MainActor.run { - isTesting = false - testProgress = nil - testResult = .successWithBandwidth(bandwidthResult) - } - } catch { - await MainActor.run { - isTesting = false - testProgress = nil - testResult = .failure(error.localizedDescription) - } - } - } - } - private func saveChanges() { guard let appEnvironment else { return } diff --git a/YatteeTests/ServiceTests.swift b/YatteeTests/ServiceTests.swift index e231d985..c6ed0853 100644 --- a/YatteeTests/ServiceTests.swift +++ b/YatteeTests/ServiceTests.swift @@ -154,101 +154,6 @@ struct LockedStorageTests { } } -// MARK: - BandwidthTestResult Tests - -@Suite("BandwidthTestResult Tests") -struct BandwidthTestResultTests { - - @Test("BandwidthTestResult with write access") - func bandwidthTestResultWithWrite() { - let result = BandwidthTestResult( - hasWriteAccess: true, - uploadSpeed: 50_000_000, // 50 MB/s - downloadSpeed: 100_000_000, // 100 MB/s - testFileSize: 5 * 1024 * 1024, // 5 MB - warning: nil - ) - - #expect(result.hasWriteAccess == true) - #expect(result.uploadSpeed == 50_000_000) - #expect(result.downloadSpeed == 100_000_000) - #expect(result.testFileSize == 5 * 1024 * 1024) - #expect(result.warning == nil) - } - - @Test("BandwidthTestResult read-only mode") - func bandwidthTestResultReadOnly() { - let result = BandwidthTestResult( - hasWriteAccess: false, - uploadSpeed: nil, - downloadSpeed: 75_000_000, - testFileSize: 5 * 1024 * 1024, - warning: "Server is read-only" - ) - - #expect(result.hasWriteAccess == false) - #expect(result.uploadSpeed == nil) - #expect(result.downloadSpeed == 75_000_000) - #expect(result.warning == "Server is read-only") - } - - @Test("BandwidthTestResult formatted speeds") - func bandwidthTestResultFormattedSpeeds() { - let result = BandwidthTestResult( - hasWriteAccess: true, - uploadSpeed: 50_000_000, - downloadSpeed: 100_000_000, - testFileSize: 5 * 1024 * 1024, - warning: nil - ) - - // Formatted strings should contain speed values (optional returns) - #expect(result.formattedDownloadSpeed != nil) - #expect(result.formattedUploadSpeed != nil) - #expect(result.formattedDownloadSpeed?.contains("/s") == true) - #expect(result.formattedUploadSpeed?.contains("/s") == true) - } - - @Test("formattedUploadSpeed nil when no upload") - func formattedUploadSpeedNil() { - let result = BandwidthTestResult( - hasWriteAccess: false, - uploadSpeed: nil, - downloadSpeed: 50_000_000, - testFileSize: 5_000_000, - warning: nil - ) - - #expect(result.formattedUploadSpeed == nil) - } - - @Test("formattedDownloadSpeed nil when no download") - func formattedDownloadSpeedNil() { - let result = BandwidthTestResult( - hasWriteAccess: false, - uploadSpeed: nil, - downloadSpeed: nil, - testFileSize: 0, - warning: "No files available" - ) - - #expect(result.formattedDownloadSpeed == nil) - } - - @Test("Warning message preserved") - func warningPreserved() { - let result = BandwidthTestResult( - hasWriteAccess: false, - uploadSpeed: nil, - downloadSpeed: nil, - testFileSize: 0, - warning: "Server appears empty, could not test download speed" - ) - - #expect(result.warning == "Server appears empty, could not test download speed") - } -} - // MARK: - MediaSourceError Tests @Suite("MediaSourceError Tests")