Files
yattee/YatteeTests/ServiceTests.swift
2026-02-08 18:33:56 +01:00

744 lines
24 KiB
Swift

//
// ServiceTests.swift
// YatteeTests
//
// Tests for service layer components including WebDAV client and storage utilities.
//
import Testing
import Foundation
@testable import Yattee
// MARK: - Storage Diagnostics Tests
@Suite("StorageDiagnostics Tests")
@MainActor
struct StorageDiagnosticsTests {
@Test("StorageUsageItem initialization")
func storageUsageItemInit() {
let item = StorageUsageItem(
name: "Downloads",
path: "/path/to/downloads",
size: 1024 * 1024 * 500, // 500 MB
fileCount: 42
)
#expect(item.name == "Downloads")
#expect(item.path == "/path/to/downloads")
#expect(item.size == 524288000)
#expect(item.fileCount == 42)
#expect(!item.id.uuidString.isEmpty)
}
@Test("StorageUsageItem is Identifiable")
func storageUsageItemIdentifiable() {
let item1 = StorageUsageItem(name: "A", path: "/a", size: 100, fileCount: 1)
let item2 = StorageUsageItem(name: "A", path: "/a", size: 100, fileCount: 1)
// Each item should have unique ID even with same content
#expect(item1.id != item2.id)
}
@Test("StorageDiagnostics formatted values")
func diagnosticsFormattedValues() {
let diagnostics = StorageDiagnostics(
items: [],
totalSize: 1024 * 1024 * 1024, // 1 GB
documentsSize: 500 * 1024 * 1024,
cachesSize: 200 * 1024 * 1024,
appSupportSize: 100 * 1024 * 1024,
tempSize: 50 * 1024 * 1024,
otherSize: 150 * 1024 * 1024
)
#expect(!diagnostics.formattedTotal.isEmpty)
#expect(!diagnostics.formattedDocuments.isEmpty)
#expect(!diagnostics.formattedCaches.isEmpty)
#expect(!diagnostics.formattedAppSupport.isEmpty)
#expect(!diagnostics.formattedTemp.isEmpty)
}
@Test("StorageDiagnostics with items")
func diagnosticsWithItems() {
let items = [
StorageUsageItem(name: "Downloads", path: "/downloads", size: 300_000_000, fileCount: 10),
StorageUsageItem(name: "Cache", path: "/cache", size: 100_000_000, fileCount: 50),
StorageUsageItem(name: "Temp", path: "/temp", size: 50_000_000, fileCount: 5)
]
let diagnostics = StorageDiagnostics(
items: items,
totalSize: 450_000_000,
documentsSize: 300_000_000,
cachesSize: 100_000_000,
appSupportSize: 0,
tempSize: 50_000_000,
otherSize: 0
)
#expect(diagnostics.items.count == 3)
#expect(diagnostics.totalSize == 450_000_000)
}
@Test("scanAppStorage returns valid diagnostics")
func scanAppStorageReturnsValid() {
let diagnostics = scanAppStorage()
#expect(diagnostics.totalSize >= 0)
#expect(diagnostics.documentsSize >= 0)
#expect(diagnostics.cachesSize >= 0)
#expect(!diagnostics.formattedTotal.isEmpty)
}
}
// MARK: - LockedStorage Tests
@Suite("LockedStorage Tests")
struct LockedStorageTests {
@Test("LockedStorage read returns value")
func readReturnsValue() {
let storage = LockedStorage(42)
let value = storage.read { $0 }
#expect(value == 42)
}
@Test("LockedStorage write modifies value")
func writeModifiesValue() {
let storage = LockedStorage(0)
storage.write { $0 += 10 }
let value = storage.read { $0 }
#expect(value == 10)
}
@Test("LockedStorage with string")
func withString() {
let storage = LockedStorage("hello")
storage.write { $0 += " world" }
let value = storage.read { $0 }
#expect(value == "hello world")
}
@Test("LockedStorage with array")
func withArray() {
let storage = LockedStorage<[Int]>([])
storage.write { $0.append(1) }
storage.write { $0.append(2) }
storage.write { $0.append(3) }
let value = storage.read { $0 }
#expect(value == [1, 2, 3])
}
@Test("LockedStorage read with transformation")
func readWithTransformation() {
let storage = LockedStorage([1, 2, 3, 4, 5])
let sum = storage.read { $0.reduce(0, +) }
#expect(sum == 15)
}
@Test("LockedStorage concurrent access")
func concurrentAccess() async {
let storage = LockedStorage(0)
await withTaskGroup(of: Void.self) { group in
for _ in 0..<100 {
group.addTask {
storage.write { $0 += 1 }
}
}
}
let finalValue = storage.read { $0 }
#expect(finalValue == 100)
}
}
// 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")
struct MediaSourceErrorTests {
@Test("MediaSourceError cases exist")
func errorCasesExist() {
let authError = MediaSourceError.authenticationFailed
let pathError = MediaSourceError.pathNotFound("/test/path")
let connectionError = MediaSourceError.connectionFailed("timeout")
let unknownError = MediaSourceError.unknown("something went wrong")
#expect(authError.errorDescription != nil)
#expect(pathError.errorDescription != nil)
#expect(connectionError.errorDescription != nil)
#expect(unknownError.errorDescription != nil)
}
@Test("MediaSourceError path not found includes path")
func pathNotFoundIncludesPath() {
let error = MediaSourceError.pathNotFound("/videos/movie.mp4")
let description = error.errorDescription ?? ""
#expect(description.contains("video") || description.contains("movie") ||
description.contains("path") || description.contains("not found") ||
description.contains("Path"))
}
@Test("MediaSourceError connection failed includes message")
func connectionFailedIncludesMessage() {
let error = MediaSourceError.connectionFailed("HTTP 500")
let description = error.errorDescription ?? ""
#expect(description.contains("500") || description.contains("connection") ||
description.contains("failed") || description.contains("HTTP") ||
description.contains("Connection"))
}
@Test("isRetryable for timeout")
func timeoutIsRetryable() {
let error = MediaSourceError.timeout
#expect(error.isRetryable == true)
}
@Test("isRetryable for noConnection")
func noConnectionIsRetryable() {
let error = MediaSourceError.noConnection
#expect(error.isRetryable == true)
}
@Test("isRetryable for connectionFailed")
func connectionFailedIsRetryable() {
let error = MediaSourceError.connectionFailed("network error")
#expect(error.isRetryable == true)
}
@Test("isRetryable for authenticationFailed")
func authenticationFailedNotRetryable() {
let error = MediaSourceError.authenticationFailed
#expect(error.isRetryable == false)
}
@Test("isRetryable for pathNotFound")
func pathNotFoundNotRetryable() {
let error = MediaSourceError.pathNotFound("/invalid")
#expect(error.isRetryable == false)
}
@Test("isRetryable for accessDenied")
func accessDeniedNotRetryable() {
let error = MediaSourceError.accessDenied
#expect(error.isRetryable == false)
}
@Test("Error equality same cases")
func equalitySameCases() {
#expect(MediaSourceError.authenticationFailed == MediaSourceError.authenticationFailed)
#expect(MediaSourceError.timeout == MediaSourceError.timeout)
#expect(MediaSourceError.noConnection == MediaSourceError.noConnection)
#expect(MediaSourceError.accessDenied == MediaSourceError.accessDenied)
}
@Test("Error equality with associated values")
func equalityAssociatedValues() {
#expect(MediaSourceError.pathNotFound("/a") == MediaSourceError.pathNotFound("/a"))
#expect(MediaSourceError.pathNotFound("/a") != MediaSourceError.pathNotFound("/b"))
#expect(MediaSourceError.connectionFailed("x") == MediaSourceError.connectionFailed("x"))
#expect(MediaSourceError.connectionFailed("x") != MediaSourceError.connectionFailed("y"))
}
@Test("All error cases have descriptions")
func allCasesHaveDescriptions() {
let errors: [MediaSourceError] = [
.connectionFailed("test"),
.authenticationFailed,
.pathNotFound("/test"),
.parsingFailed("xml error"),
.notADirectory,
.invalidResponse,
.bookmarkResolutionFailed,
.accessDenied,
.timeout,
.noConnection,
.unknown("mystery")
]
for error in errors {
#expect(error.errorDescription != nil)
#expect(!error.errorDescription!.isEmpty)
}
}
}
// MARK: - MediaSource Configuration Tests
@Suite("MediaSource Configuration Tests")
struct MediaSourceConfigurationTests {
@Test("WebDAV factory method")
func webdavFactory() {
let source = MediaSource.webdav(
name: "My NAS",
url: URL(string: "https://nas.local:5006/webdav")!,
username: "admin"
)
#expect(source.type == .webdav)
#expect(source.name == "My NAS")
#expect(source.username == "admin")
#expect(source.url.absoluteString == "https://nas.local:5006/webdav")
#expect(source.isEnabled == true)
#expect(source.requiresAuthentication == true)
}
@Test("WebDAV without username")
func webdavWithoutUsername() {
let source = MediaSource.webdav(
name: "Public NAS",
url: URL(string: "https://public.nas/webdav")!
)
#expect(source.username == nil)
#expect(source.requiresAuthentication == false)
}
@Test("LocalFolder factory method")
func localFolderFactory() {
let url = URL(fileURLWithPath: "/Users/test/Videos")
let source = MediaSource.localFolder(
name: "Videos",
url: url,
bookmarkData: Data([0x01, 0x02, 0x03])
)
#expect(source.type == .localFolder)
#expect(source.name == "Videos")
#expect(source.bookmarkData != nil)
#expect(source.requiresAuthentication == false)
}
@Test("MediaSourceType displayName")
func mediaSourceTypeDisplayName() {
#expect(!MediaSourceType.webdav.displayName.isEmpty)
#expect(!MediaSourceType.localFolder.displayName.isEmpty)
}
@Test("MediaSourceType systemImage")
func mediaSourceTypeSystemImage() {
#expect(!MediaSourceType.webdav.systemImage.isEmpty)
#expect(!MediaSourceType.localFolder.systemImage.isEmpty)
}
@Test("MediaSourceType CaseIterable")
func mediaSourceTypeCaseIterable() {
let allCases = MediaSourceType.allCases
#expect(allCases.contains(.webdav))
#expect(allCases.contains(.localFolder))
#expect(allCases.contains(.smb))
#expect(allCases.count == 3)
}
@Test("MediaSource urlDisplayString WebDAV")
func urlDisplayStringWebDAV() {
let source = MediaSource.webdav(
name: "NAS",
url: URL(string: "https://nas.synology.me/webdav")!
)
#expect(source.urlDisplayString == "nas.synology.me")
}
@Test("MediaSource urlDisplayString LocalFolder")
func urlDisplayStringLocalFolder() {
let source = MediaSource.localFolder(
name: "Movies",
url: URL(fileURLWithPath: "/Users/test/Movies")
)
#expect(source.urlDisplayString == "Movies")
}
@Test("MediaSource is Identifiable")
func mediaSourceIdentifiable() {
let source1 = MediaSource.webdav(name: "A", url: URL(string: "https://a.com")!)
let source2 = MediaSource.webdav(name: "A", url: URL(string: "https://a.com")!)
// Each source gets unique UUID
#expect(source1.id != source2.id)
}
@Test("MediaSource is Codable")
func mediaSourceCodable() throws {
let source = MediaSource.webdav(
name: "Test NAS",
url: URL(string: "https://nas.local")!,
username: "user"
)
let encoded = try JSONEncoder().encode(source)
let decoded = try JSONDecoder().decode(MediaSource.self, from: encoded)
#expect(decoded.name == source.name)
#expect(decoded.type == source.type)
#expect(decoded.url == source.url)
#expect(decoded.username == source.username)
}
}
// MARK: - MediaFile Tests
@Suite("MediaFile Tests")
struct MediaFileTests {
private func createTestSource() -> MediaSource {
MediaSource.webdav(name: "Test", url: URL(string: "https://nas.local")!)
}
@Test("MediaFile initialization")
func mediaFileInit() {
let source = createTestSource()
let file = MediaFile(
source: source,
path: "/videos/movie.mp4",
name: "movie.mp4",
isDirectory: false,
size: 104857600, // 100 MB
modifiedDate: Date()
)
#expect(file.name == "movie.mp4")
#expect(file.path == "/videos/movie.mp4")
#expect(file.isDirectory == false)
#expect(file.size == 104857600)
}
@Test("MediaFile directory type")
func mediaFileDirectory() {
let source = MediaSource.localFolder(
name: "Videos",
url: URL(fileURLWithPath: "/Users/test/Videos")
)
let folder = MediaFile(
source: source,
path: "/Movies",
name: "Movies",
isDirectory: true,
size: nil,
modifiedDate: nil
)
#expect(folder.isDirectory == true)
#expect(folder.size == nil)
}
@Test("MediaFile is Identifiable")
func mediaFileIdentifiable() {
let source = createTestSource()
let file = MediaFile(
source: source,
path: "/video.mp4",
name: "video.mp4",
isDirectory: false,
size: 1000,
modifiedDate: nil
)
#expect(!file.id.isEmpty)
#expect(file.id.contains(source.id.uuidString))
}
@Test("MediaFile Hashable")
func mediaFileHashable() {
let source = createTestSource()
let file1 = MediaFile(
source: source,
path: "/video.mp4",
name: "video.mp4",
isDirectory: false,
size: 1000,
modifiedDate: nil
)
let file2 = MediaFile(
source: source,
path: "/video.mp4",
name: "video.mp4",
isDirectory: false,
size: 1000,
modifiedDate: nil
)
// Same path and source should be equal
#expect(file1 == file2)
var set = Set<MediaFile>()
set.insert(file1)
#expect(set.contains(file2))
}
@Test("MediaFile isVideo for video files")
func isVideoForVideoFiles() {
let source = createTestSource()
let extensions = ["mp4", "mkv", "avi", "mov", "webm", "flv", "m4v"]
for ext in extensions {
let file = MediaFile(
source: source,
path: "/movie.\(ext)",
name: "movie.\(ext)",
isDirectory: false
)
#expect(file.isVideo == true, "Expected .\(ext) to be video")
}
}
@Test("MediaFile isVideo false for directories")
func isVideoFalseForDirectories() {
let source = createTestSource()
let folder = MediaFile(
source: source,
path: "/Videos",
name: "Videos",
isDirectory: true
)
#expect(folder.isVideo == false)
}
@Test("MediaFile isAudio for audio files")
func isAudioForAudioFiles() {
let source = createTestSource()
let extensions = ["mp3", "m4a", "flac", "wav", "ogg", "opus", "aac"]
for ext in extensions {
let file = MediaFile(
source: source,
path: "/song.\(ext)",
name: "song.\(ext)",
isDirectory: false
)
#expect(file.isAudio == true, "Expected .\(ext) to be audio")
}
}
@Test("MediaFile isPlayable")
func isPlayable() {
let source = createTestSource()
let videoFile = MediaFile(source: source, path: "/movie.mp4", name: "movie.mp4", isDirectory: false)
let audioFile = MediaFile(source: source, path: "/song.mp3", name: "song.mp3", isDirectory: false)
let textFile = MediaFile(source: source, path: "/readme.txt", name: "readme.txt", isDirectory: false)
let folder = MediaFile(source: source, path: "/Movies", name: "Movies", isDirectory: true)
#expect(videoFile.isPlayable == true)
#expect(audioFile.isPlayable == true)
#expect(textFile.isPlayable == false)
#expect(folder.isPlayable == false)
}
@Test("MediaFile fileExtension")
func fileExtension() {
let source = createTestSource()
let file1 = MediaFile(source: source, path: "/video.MP4", name: "video.MP4", isDirectory: false)
let file2 = MediaFile(source: source, path: "/movie.MKV", name: "movie.MKV", isDirectory: false)
// Extensions should be lowercase
#expect(file1.fileExtension == "mp4")
#expect(file2.fileExtension == "mkv")
}
@Test("MediaFile formattedSize")
func formattedSize() {
let source = createTestSource()
let smallFile = MediaFile(source: source, path: "/small.txt", name: "small.txt", isDirectory: false, size: 1024)
let largeFile = MediaFile(source: source, path: "/large.mp4", name: "large.mp4", isDirectory: false, size: 1_500_000_000)
let noSize = MediaFile(source: source, path: "/unknown.dat", name: "unknown.dat", isDirectory: false, size: nil)
#expect(smallFile.formattedSize != nil)
#expect(largeFile.formattedSize != nil)
#expect(noSize.formattedSize == nil)
}
@Test("MediaFile systemImage")
func systemImage() {
let source = createTestSource()
let folder = MediaFile(source: source, path: "/Dir", name: "Dir", isDirectory: true)
let video = MediaFile(source: source, path: "/movie.mp4", name: "movie.mp4", isDirectory: false)
let audio = MediaFile(source: source, path: "/song.mp3", name: "song.mp3", isDirectory: false)
let other = MediaFile(source: source, path: "/doc.pdf", name: "doc.pdf", isDirectory: false)
#expect(folder.systemImage == "folder.fill")
#expect(video.systemImage == "film")
#expect(audio.systemImage == "music.note")
#expect(other.systemImage == "doc")
}
@Test("MediaFile url construction")
func urlConstruction() {
let source = MediaSource.webdav(name: "NAS", url: URL(string: "https://nas.local/webdav")!)
let file = MediaFile(source: source, path: "/videos/movie.mp4", name: "movie.mp4", isDirectory: false)
#expect(file.url.absoluteString.contains("nas.local"))
#expect(file.url.absoluteString.contains("movie.mp4"))
}
@Test("MediaFile toVideo conversion")
func toVideoConversion() {
let source = MediaSource.webdav(name: "NAS", url: URL(string: "https://nas.local")!)
let modDate = Date()
let file = MediaFile(
source: source,
path: "/movies/My Movie.mp4",
name: "My Movie.mp4",
isDirectory: false,
size: 1_000_000,
modifiedDate: modDate
)
let video = file.toVideo()
#expect(video.title == "My Movie")
#expect(video.author.name == "NAS")
#expect(video.publishedAt == modDate)
#expect(video.isLive == false)
}
@Test("MediaFile toStream conversion")
func toStreamConversion() {
let source = MediaSource.webdav(name: "NAS", url: URL(string: "https://nas.local")!)
let file = MediaFile(
source: source,
path: "/movie.mkv",
name: "movie.mkv",
isDirectory: false
)
let stream = file.toStream(authHeaders: ["Authorization": "Basic abc123"])
#expect(stream.format == "mkv")
#expect(stream.httpHeaders?["Authorization"] == "Basic abc123")
}
@Test("MediaFile preview samples")
func previewSamples() {
let file = MediaFile.preview
let folder = MediaFile.folderPreview
#expect(file.isDirectory == false)
#expect(file.isVideo == true)
#expect(folder.isDirectory == true)
}
@Test("MediaFile video extensions coverage")
func videoExtensionsCoverage() {
// Verify all expected video extensions are included
let expected = ["mp4", "m4v", "mov", "mkv", "avi", "webm", "wmv", "flv", "mpg", "mpeg", "3gp", "ts", "vob"]
for ext in expected {
#expect(MediaFile.videoExtensions.contains(ext), "Missing video extension: \(ext)")
}
}
@Test("MediaFile audio extensions coverage")
func audioExtensionsCoverage() {
// Verify all expected audio extensions are included
let expected = ["mp3", "m4a", "aac", "flac", "wav", "ogg", "opus", "wma", "aiff"]
for ext in expected {
#expect(MediaFile.audioExtensions.contains(ext), "Missing audio extension: \(ext)")
}
}
}