// // ModelTests.swift // YatteeTests // // Tests for core model types. // import Testing import Foundation @testable import Yattee // MARK: - ContentSource Tests @Suite("ContentSource Tests") @MainActor struct ContentSourceTests { @Test("Global source equality") func globalEquality() { let source1 = ContentSource.global(provider: ContentSource.youtubeProvider) let source2 = ContentSource.global(provider: ContentSource.youtubeProvider) #expect(source1 == source2) } @Test("Federated source with same instance are equal") func federatedEqualitySameInstance() { let url = URL(string: "https://peertube.example.com")! let source1 = ContentSource.federated(provider: ContentSource.peertubeProvider, instance: url) let source2 = ContentSource.federated(provider: ContentSource.peertubeProvider, instance: url) #expect(source1 == source2) } @Test("Federated sources with different instances are not equal") func federatedEqualityDifferentInstances() { let url1 = URL(string: "https://peertube1.example.com")! let url2 = URL(string: "https://peertube2.example.com")! let source1 = ContentSource.federated(provider: ContentSource.peertubeProvider, instance: url1) let source2 = ContentSource.federated(provider: ContentSource.peertubeProvider, instance: url2) #expect(source1 != source2) } @Test("Global and Federated are not equal") func globalNotEqualToFederated() { let global = ContentSource.global(provider: ContentSource.youtubeProvider) let federated = ContentSource.federated(provider: ContentSource.peertubeProvider, instance: URL(string: "https://example.com")!) #expect(global != federated) } @Test("ContentSource display names") func displayNames() { #expect(ContentSource.global(provider: ContentSource.youtubeProvider).displayName == "YouTube") let peertubeURL = URL(string: "https://framatube.org")! #expect(ContentSource.federated(provider: ContentSource.peertubeProvider, instance: peertubeURL).displayName == "framatube.org") } @Test("ContentSource short names") func shortNames() { #expect(ContentSource.global(provider: ContentSource.youtubeProvider).shortName == "YT") let peertubeURL = URL(string: "https://framatube.org")! #expect(ContentSource.federated(provider: ContentSource.peertubeProvider, instance: peertubeURL).shortName == "framatub") } @Test("ContentSource is Codable") func codable() throws { let sources: [ContentSource] = [ .global(provider: ContentSource.youtubeProvider), .federated(provider: ContentSource.peertubeProvider, instance: URL(string: "https://example.com")!) ] for source in sources { let encoded = try JSONEncoder().encode(source) let decoded = try JSONDecoder().decode(ContentSource.self, from: encoded) #expect(source == decoded) } } @Test("ContentSource sorting - Global comes before Federated") func sorting() { let global = ContentSource.global(provider: ContentSource.youtubeProvider) let federated = ContentSource.federated(provider: ContentSource.peertubeProvider, instance: URL(string: "https://example.com")!) #expect(global < federated) #expect(!(federated < global)) } } // MARK: - VideoID Tests @Suite("VideoID Tests") @MainActor struct VideoIDTests { @Test("Global VideoID creation") func globalCreation() { let videoID = VideoID.global("dQw4w9WgXcQ") #expect(videoID.videoID == "dQw4w9WgXcQ") if case .global(let provider) = videoID.source { #expect(provider == ContentSource.youtubeProvider) } else { Issue.record("Expected global source") } #expect(videoID.uuid == nil) } @Test("Federated VideoID creation") func federatedCreation() { let instance = URL(string: "https://framatube.org")! let videoID = VideoID.federated("123", instance: instance, uuid: "abc-def") #expect(videoID.videoID == "123") if case .federated(let provider, let url) = videoID.source { #expect(provider == ContentSource.peertubeProvider) #expect(url == instance) } else { Issue.record("Expected federated source") } #expect(videoID.uuid == "abc-def") } @Test("VideoID identifiable ID format") func identifiableID() { let ytID = VideoID.global("abc123") #expect(ytID.id == "global:youtube:abc123") let ptID = VideoID.federated("456", instance: URL(string: "https://example.com")!) #expect(ptID.id == "federated:peertube:example.com:456") } } // MARK: - Video Tests @Suite("Video Tests") @MainActor struct VideoTests { @Test("Video formatted duration - minutes and seconds") func formattedDurationMinutesSeconds() { let video = makeVideo(duration: 185) // 3:05 #expect(video.formattedDuration == "3:05") } @Test("Video formatted duration - hours") func formattedDurationHours() { let video = makeVideo(duration: 3725) // 1:02:05 #expect(video.formattedDuration == "1:02:05") } @Test("Video formatted duration - live shows LIVE") func formattedDurationLive() { let video = makeVideo(duration: 0, isLive: true) #expect(video.formattedDuration == "LIVE") } @Test("Video formatted view count - thousands") func formattedViewCountThousands() { let video = makeVideo(viewCount: 1500) #expect(video.formattedViewCount == "1.5K") } @Test("Video formatted view count - millions") func formattedViewCountMillions() { let video = makeVideo(viewCount: 2_500_000) #expect(video.formattedViewCount == "2.5M") } @Test("Video formatted view count - exact thousands") func formattedViewCountExactThousands() { let video = makeVideo(viewCount: 1000) #expect(video.formattedViewCount == "1K") } @Test("Video best thumbnail returns highest quality") func bestThumbnail() { let thumbnails = [ Thumbnail(url: URL(string: "https://example.com/default.jpg")!, quality: .default), Thumbnail(url: URL(string: "https://example.com/maxres.jpg")!, quality: .maxres), Thumbnail(url: URL(string: "https://example.com/high.jpg")!, quality: .high), ] let video = makeVideo(thumbnails: thumbnails) #expect(video.bestThumbnail?.quality == .maxres) } private func makeVideo( duration: TimeInterval = 100, isLive: Bool = false, viewCount: Int? = nil, thumbnails: [Thumbnail] = [] ) -> Video { Video( id: .global("test"), title: "Test Video", description: nil, author: Author(id: "channel", name: "Test Channel"), duration: duration, publishedAt: nil, publishedText: nil, viewCount: viewCount, likeCount: nil, thumbnails: thumbnails, isLive: isLive, isUpcoming: false, scheduledStartTime: nil ) } } // MARK: - Instance Tests @Suite("Instance Tests") @MainActor struct InstanceTests { @Test("Instance URL validation - valid HTTPS") func validateURLValidHTTPS() { let url = Instance.validateURL("https://invidious.io") #expect(url != nil) #expect(url?.scheme == "https") } @Test("Instance URL validation - preserves explicit HTTP for local servers") func validateURLPreservesHTTP() { // HTTP is preserved for local/private network servers (e.g., yt-dlp server) let url = Instance.validateURL("http://invidious.io") #expect(url?.scheme == "http") } @Test("Instance URL validation - adds HTTPS if missing") func validateURLAddsScheme() { let url = Instance.validateURL("invidious.io") #expect(url?.scheme == "https") } @Test("Instance URL validation - removes trailing slash") func validateURLRemovesTrailingSlash() { let url = Instance.validateURL("https://invidious.io/") #expect(url?.path == "" || !url!.absoluteString.hasSuffix("/")) } @Test("Instance URL validation - handles edge cases") func validateURLEdgeCases() { // URLComponents is lenient and encodes spaces let urlWithSpaces = Instance.validateURL("example with spaces") #expect(urlWithSpaces != nil) // Gets URL-encoded // Verifies scheme is added let simpleHost = Instance.validateURL("invidious.io") #expect(simpleHost?.scheme == "https") } @Test("Instance display name uses custom name if set") func displayNameCustom() { let instance = Instance( type: .invidious, url: URL(string: "https://invidious.io")!, name: "My Instance" ) #expect(instance.displayName == "My Instance") } @Test("Instance display name falls back to host") func displayNameFallback() { let instance = Instance( type: .invidious, url: URL(string: "https://invidious.io")! ) #expect(instance.displayName == "invidious.io") } @Test("Instance isYouTubeInstance for Invidious") func isYouTubeInstanceInvidious() { let instance = Instance(type: .invidious, url: URL(string: "https://example.com")!) #expect(instance.isYouTubeInstance == true) #expect(instance.isPeerTubeInstance == false) } @Test("Instance isYouTubeInstance for Piped") func isYouTubeInstancePiped() { let instance = Instance(type: .piped, url: URL(string: "https://example.com")!) #expect(instance.isYouTubeInstance == true) #expect(instance.isPeerTubeInstance == false) } @Test("Instance isPeerTubeInstance") func isPeerTubeInstance() { let instance = Instance(type: .peertube, url: URL(string: "https://example.com")!) #expect(instance.isYouTubeInstance == false) #expect(instance.isPeerTubeInstance == true) } } // MARK: - Channel Tests @Suite("Channel Tests") @MainActor struct ChannelTests { @Test("Channel formatted subscriber count") func formattedSubscriberCount() { let channel = Channel( id: .global("test"), name: "Test Channel", subscriberCount: 1_500_000 ) #expect(channel.formattedSubscriberCount == "1.5M") } @Test("ChannelID identifiable ID format") func channelIDFormat() { let ytID = ChannelID.global("UC123") #expect(ytID.id == "global:youtube:UC123") let ptID = ChannelID.federated("channel", instance: URL(string: "https://example.com")!) #expect(ptID.id == "federated:peertube:example.com:channel") } } // MARK: - Stream Tests @Suite("Stream Tests") @MainActor struct StreamTests { @Test("StreamResolution comparison") func resolutionComparison() { #expect(StreamResolution.p720 < StreamResolution.p1080) #expect(StreamResolution.p1080 < StreamResolution.p2160) #expect(!(StreamResolution.p1080 < StreamResolution.p720)) } @Test("StreamResolution from height label") func resolutionFromLabel() { let res720 = StreamResolution(heightLabel: "720p") #expect(res720?.height == 720) let res1080 = StreamResolution(heightLabel: "1080") #expect(res1080?.height == 1080) } @Test("Stream quality label for video") func qualityLabelVideo() { let stream = Stream( url: URL(string: "https://example.com/video.mp4")!, resolution: .p1080, format: "mp4" ) #expect(stream.qualityLabel == "1080p") } @Test("Stream quality label for audio") func qualityLabelAudio() { let stream = Stream( url: URL(string: "https://example.com/audio.m4a")!, resolution: nil, format: "m4a", isAudioOnly: true ) #expect(stream.qualityLabel == "Audio") } @Test("Stream isNativelyPlayable for MP4 H264") func nativelyPlayableMp4() { let stream = Stream( url: URL(string: "https://example.com/video.mp4")!, resolution: .p1080, format: "mp4", videoCodec: "avc1.4d401f" ) #expect(stream.isNativelyPlayable == true) } @Test("Stream isNativelyPlayable for WebM VP9") func nativelyPlayableWebm() { let stream = Stream( url: URL(string: "https://example.com/video.webm")!, resolution: .p1080, format: "webm", videoCodec: "vp9" ) #expect(stream.isNativelyPlayable == false) } } // MARK: - Playlist Tests @Suite("Playlist Tests") @MainActor struct PlaylistTests { @Test("PlaylistID local vs remote") func playlistIDLocalVsRemote() { let localID = PlaylistID.local("my-playlist") #expect(localID.isLocal == true) #expect(localID.id == "local:my-playlist") let remoteID = PlaylistID.global("PLtest123") #expect(remoteID.isLocal == false) #expect(remoteID.id == "global:youtube:PLtest123") } } // MARK: - Caption Tests @Suite("Caption Tests") struct CaptionTests { @Test("Caption isAutoGenerated detection") func isAutoGenerated() { let autoCaption = Caption( label: "English (auto-generated)", languageCode: "en", url: URL(string: "https://example.com/caption.vtt")! ) #expect(autoCaption.isAutoGenerated == true) let manualCaption = Caption( label: "English", languageCode: "en", url: URL(string: "https://example.com/caption.vtt")! ) #expect(manualCaption.isAutoGenerated == false) } @Test("Caption baseLanguageCode extracts base code") func baseLanguageCode() { let enUS = Caption( label: "English (US)", languageCode: "en-US", url: URL(string: "https://example.com/caption.vtt")! ) #expect(enUS.baseLanguageCode == "en") let deDE = Caption( label: "German", languageCode: "de-DE", url: URL(string: "https://example.com/caption.vtt")! ) #expect(deDE.baseLanguageCode == "de") let simple = Caption( label: "French", languageCode: "fr", url: URL(string: "https://example.com/caption.vtt")! ) #expect(simple.baseLanguageCode == "fr") } @Test("Caption id is unique") func captionID() { let caption = Caption( label: "English", languageCode: "en", url: URL(string: "https://example.com/caption.vtt")! ) #expect(caption.id == "en:English") } @Test("Caption displayName strips auto-generated suffix") func displayName() { let autoCaption = Caption( label: "English (auto-generated)", languageCode: "en", url: URL(string: "https://example.com/caption.vtt")! ) // Should return localized name or stripped label #expect(!autoCaption.displayName.contains("auto-generated")) } @Test("Caption is Codable") func codable() throws { let caption = Caption( label: "Spanish", languageCode: "es", url: URL(string: "https://example.com/caption.vtt")! ) let encoded = try JSONEncoder().encode(caption) let decoded = try JSONDecoder().decode(Caption.self, from: encoded) #expect(caption == decoded) } @Test("Caption is Hashable") func hashable() { let caption1 = Caption( label: "English", languageCode: "en", url: URL(string: "https://example.com/caption1.vtt")! ) let caption2 = Caption( label: "English", languageCode: "en", url: URL(string: "https://example.com/caption1.vtt")! ) let caption3 = Caption( label: "French", languageCode: "fr", url: URL(string: "https://example.com/caption2.vtt")! ) var set = Set