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

824 lines
25 KiB
Swift

//
// DataTests.swift
// YatteeTests
//
// Tests for the local data persistence layer.
//
import Testing
import Foundation
@testable import Yattee
@MainActor
@Suite("Data Layer Tests")
struct DataTests {
// MARK: - Watch Entry Tests
@Suite("Watch Entry")
struct WatchEntryTests {
@Test("Progress calculation")
@MainActor
func progressCalculation() {
let entry = WatchEntry(
videoID: "test123",
sourceRawValue: "youtube",
title: "Test Video",
authorName: "Test Channel",
authorID: "channel123",
duration: 600, // 10 minutes
watchedSeconds: 300 // 5 minutes
)
#expect(entry.progress == 0.5)
}
@Test("Auto-finish at 90%")
@MainActor
func autoFinishAt90Percent() {
let entry = WatchEntry(
videoID: "test123",
sourceRawValue: "youtube",
title: "Test Video",
authorName: "Test Channel",
authorID: "channel123",
duration: 100
)
#expect(!entry.isFinished)
entry.updateProgress(seconds: 90)
#expect(entry.isFinished)
#expect(entry.progress >= 0.9)
}
@Test("Reset progress")
@MainActor
func resetProgress() {
let entry = WatchEntry(
videoID: "test123",
sourceRawValue: "youtube",
title: "Test Video",
authorName: "Test Channel",
authorID: "channel123",
duration: 100,
watchedSeconds: 90,
isFinished: true
)
entry.resetProgress()
#expect(entry.watchedSeconds == 0)
#expect(!entry.isFinished)
}
@Test("Content source YouTube")
@MainActor
func contentSourceYouTube() {
let entry = WatchEntry(
videoID: "abc123",
sourceRawValue: "global",
title: "Test",
authorName: "Channel",
authorID: "ch1",
duration: 100
)
if case .global = entry.contentSource {
// Expected
} else {
Issue.record("Expected global source")
}
}
@Test("Content source Federated")
@MainActor
func contentSourceFederated() {
let entry = WatchEntry(
videoID: "uuid123",
sourceRawValue: "federated",
instanceURLString: "https://peertube.example.com",
title: "Test",
authorName: "Channel",
authorID: "ch1",
duration: 100
)
if case .federated(_, let instance) = entry.contentSource {
#expect(instance.host == "peertube.example.com")
} else {
Issue.record("Expected federated source")
}
}
@Test("Remaining time formatting")
@MainActor
func remainingTimeFormatting() {
let entry = WatchEntry(
videoID: "test",
sourceRawValue: "youtube",
title: "Test",
authorName: "Channel",
authorID: "ch1",
duration: 600, // 10 minutes
watchedSeconds: 300 // 5 minutes watched
)
// 5 minutes remaining = "5:00"
#expect(entry.remainingTime == "5:00")
}
@Test("Remaining time with seconds")
@MainActor
func remainingTimeWithSeconds() {
let entry = WatchEntry(
videoID: "test",
sourceRawValue: "youtube",
title: "Test",
authorName: "Channel",
authorID: "ch1",
duration: 125, // 2:05
watchedSeconds: 60 // 1 minute watched
)
// 65 seconds remaining = "1:05"
#expect(entry.remainingTime == "1:05")
}
@Test("Thumbnail URL conversion")
@MainActor
func thumbnailURL() {
let entry = WatchEntry(
videoID: "test",
sourceRawValue: "youtube",
title: "Test",
authorName: "Channel",
authorID: "ch1",
duration: 100,
thumbnailURLString: "https://example.com/thumb.jpg"
)
#expect(entry.thumbnailURL?.absoluteString == "https://example.com/thumb.jpg")
}
@Test("Thumbnail URL nil for invalid string")
@MainActor
func thumbnailURLNil() {
let entry = WatchEntry(
videoID: "test",
sourceRawValue: "youtube",
title: "Test",
authorName: "Channel",
authorID: "ch1",
duration: 100,
thumbnailURLString: nil
)
#expect(entry.thumbnailURL == nil)
}
@Test("Mark as finished")
@MainActor
func markAsFinished() {
let entry = WatchEntry(
videoID: "test",
sourceRawValue: "youtube",
title: "Test",
authorName: "Channel",
authorID: "ch1",
duration: 100
)
#expect(!entry.isFinished)
entry.markAsFinished()
#expect(entry.isFinished)
}
}
// MARK: - Bookmark Tests
@Suite("Bookmark")
struct BookmarkTests {
@Test("Formatted duration hours")
@MainActor
func formattedDurationHours() {
let bookmark = Bookmark(
videoID: "test",
sourceRawValue: "youtube",
title: "Long Video",
authorName: "Channel",
authorID: "ch1",
duration: 3661 // 1:01:01
)
#expect(bookmark.formattedDuration == "1:01:01")
}
@Test("Formatted duration minutes")
@MainActor
func formattedDurationMinutes() {
let bookmark = Bookmark(
videoID: "test",
sourceRawValue: "youtube",
title: "Short Video",
authorName: "Channel",
authorID: "ch1",
duration: 125 // 2:05
)
#expect(bookmark.formattedDuration == "2:05")
}
@Test("Live shows LIVE")
@MainActor
func liveShowsLive() {
let bookmark = Bookmark(
videoID: "test",
sourceRawValue: "youtube",
title: "Live Stream",
authorName: "Channel",
authorID: "ch1",
duration: 0,
isLive: true
)
#expect(bookmark.formattedDuration == "LIVE")
}
@Test("Content source Global")
@MainActor
func contentSourceGlobal() {
let bookmark = Bookmark(
videoID: "abc123",
sourceRawValue: "global",
title: "Test",
authorName: "Channel",
authorID: "ch1",
duration: 100
)
if case .global = bookmark.contentSource {
// Expected
} else {
Issue.record("Expected global source")
}
}
@Test("Content source Federated")
@MainActor
func contentSourceFederated() {
let bookmark = Bookmark(
videoID: "uuid123",
sourceRawValue: "federated",
instanceURLString: "https://peertube.example.com",
title: "Test",
authorName: "Channel",
authorID: "ch1",
duration: 100
)
if case .federated(_, let instance) = bookmark.contentSource {
#expect(instance.host == "peertube.example.com")
} else {
Issue.record("Expected federated source")
}
}
@Test("Thumbnail URL conversion")
@MainActor
func thumbnailURL() {
let bookmark = Bookmark(
videoID: "test",
sourceRawValue: "youtube",
title: "Test",
authorName: "Channel",
authorID: "ch1",
duration: 100,
thumbnailURLString: "https://example.com/thumb.jpg"
)
#expect(bookmark.thumbnailURL?.absoluteString == "https://example.com/thumb.jpg")
}
@Test("Zero duration shows empty string")
@MainActor
func zeroDuration() {
let bookmark = Bookmark(
videoID: "test",
sourceRawValue: "youtube",
title: "Test",
authorName: "Channel",
authorID: "ch1",
duration: 0,
isLive: false
)
#expect(bookmark.formattedDuration == "")
}
}
// MARK: - Local Playlist Tests
@Suite("Local Playlist")
struct LocalPlaylistTests {
@Test("Video count")
@MainActor
func videoCount() {
let playlist = LocalPlaylist(title: "My Playlist")
#expect(playlist.videoCount == 0)
}
@Test("Total duration formatting")
@MainActor
func totalDurationFormatting() {
let playlist = LocalPlaylist(title: "My Playlist")
// Empty playlist
#expect(playlist.formattedTotalDuration == "0 min")
}
@Test("Total duration with hours")
@MainActor
func totalDurationWithHours() {
let playlist = LocalPlaylist(title: "Long Playlist")
let item = LocalPlaylistItem(
sortOrder: 0,
videoID: "video1",
sourceRawValue: "youtube",
title: "Long Video",
authorName: "Channel",
authorID: "ch1",
duration: 7200 // 2 hours
)
item.playlist = playlist
playlist.items?.append(item)
#expect(playlist.formattedTotalDuration == "2h 0m")
}
@Test("Contains video check")
@MainActor
func containsVideoCheck() {
let playlist = LocalPlaylist(title: "Test")
let item = LocalPlaylistItem(
sortOrder: 0,
videoID: "abc123",
sourceRawValue: "youtube",
title: "Video",
authorName: "Channel",
authorID: "ch1",
duration: 100
)
item.playlist = playlist
playlist.items?.append(item)
#expect(playlist.contains(videoID: "abc123"))
#expect(!playlist.contains(videoID: "xyz789"))
}
@Test("Sorted items by order")
@MainActor
func sortedItems() {
let playlist = LocalPlaylist(title: "Test")
// Add items out of order
let item2 = LocalPlaylistItem(
sortOrder: 2,
videoID: "video2",
sourceRawValue: "youtube",
title: "Second",
authorName: "Channel",
authorID: "ch1",
duration: 100
)
let item1 = LocalPlaylistItem(
sortOrder: 1,
videoID: "video1",
sourceRawValue: "youtube",
title: "First",
authorName: "Channel",
authorID: "ch1",
duration: 100
)
let item3 = LocalPlaylistItem(
sortOrder: 3,
videoID: "video3",
sourceRawValue: "youtube",
title: "Third",
authorName: "Channel",
authorID: "ch1",
duration: 100
)
item1.playlist = playlist
item2.playlist = playlist
item3.playlist = playlist
playlist.items?.append(item2)
playlist.items?.append(item1)
playlist.items?.append(item3)
let sorted = playlist.sortedItems
#expect(sorted.count == 3)
#expect(sorted[0].videoID == "video1")
#expect(sorted[1].videoID == "video2")
#expect(sorted[2].videoID == "video3")
}
@Test("Thumbnail URL from first sorted item")
@MainActor
func thumbnailURL() {
let playlist = LocalPlaylist(title: "Test")
let item1 = LocalPlaylistItem(
sortOrder: 1,
videoID: "video1",
sourceRawValue: "youtube",
title: "First",
authorName: "Channel",
authorID: "ch1",
duration: 100,
thumbnailURLString: "https://example.com/thumb1.jpg"
)
let item2 = LocalPlaylistItem(
sortOrder: 0,
videoID: "video2",
sourceRawValue: "youtube",
title: "Actually First",
authorName: "Channel",
authorID: "ch1",
duration: 100,
thumbnailURLString: "https://example.com/thumb2.jpg"
)
item1.playlist = playlist
item2.playlist = playlist
playlist.items?.append(item1)
playlist.items?.append(item2)
// Should get thumbnail from item with lowest sortOrder
#expect(playlist.thumbnailURL?.absoluteString == "https://example.com/thumb2.jpg")
}
@Test("Items is optional for CloudKit compatibility")
@MainActor
func itemsOptional() {
let playlist = LocalPlaylist(title: "Empty")
// items should be initialized as empty array, not nil
#expect(playlist.items != nil)
#expect(playlist.items?.isEmpty == true)
}
}
// MARK: - Subscription Tests
@Suite("Subscription")
struct SubscriptionTests {
@Test("Formatted subscriber count")
@MainActor
func formattedSubscriberCount() {
let sub = Subscription(
channelID: "ch123",
sourceRawValue: "youtube",
name: "Popular Channel",
subscriberCount: 1_500_000
)
#expect(sub.formattedSubscriberCount == "1.5M")
}
@Test("No subscriber count")
@MainActor
func noSubscriberCount() {
let sub = Subscription(
channelID: "ch123",
sourceRawValue: "youtube",
name: "New Channel"
)
#expect(sub.formattedSubscriberCount == nil)
}
@Test("Content source Global")
@MainActor
func contentSourceGlobal() {
let sub = Subscription(
channelID: "ch123",
sourceRawValue: "global",
name: "Channel"
)
if case .global = sub.contentSource {
// Expected
} else {
Issue.record("Expected global source")
}
}
@Test("Content source Federated")
@MainActor
func contentSourceFederated() {
let sub = Subscription(
channelID: "ch123",
sourceRawValue: "federated",
instanceURLString: "https://peertube.example.com",
name: "Channel"
)
if case .federated(_, let instance) = sub.contentSource {
#expect(instance.host == "peertube.example.com")
} else {
Issue.record("Expected federated source")
}
}
@Test("Avatar URL conversion")
@MainActor
func avatarURL() {
let sub = Subscription(
channelID: "ch123",
sourceRawValue: "youtube",
name: "Channel",
avatarURLString: "https://example.com/avatar.jpg"
)
#expect(sub.avatarURL?.absoluteString == "https://example.com/avatar.jpg")
}
@Test("Banner URL conversion")
@MainActor
func bannerURL() {
let sub = Subscription(
channelID: "ch123",
sourceRawValue: "youtube",
name: "Channel",
bannerURLString: "https://example.com/banner.jpg"
)
#expect(sub.bannerURL?.absoluteString == "https://example.com/banner.jpg")
}
@Test("Update from channel")
@MainActor
func updateFromChannel() {
let sub = Subscription(
channelID: "ch123",
sourceRawValue: "youtube",
name: "Old Name",
subscriberCount: 1000
)
let channel = Channel(
id: .global("ch123"),
name: "New Name",
description: "Updated description",
subscriberCount: 2000,
thumbnailURL: URL(string: "https://example.com/new-avatar.jpg"),
bannerURL: URL(string: "https://example.com/new-banner.jpg"),
isVerified: true
)
sub.update(from: channel)
#expect(sub.name == "New Name")
#expect(sub.channelDescription == "Updated description")
#expect(sub.subscriberCount == 2000)
#expect(sub.avatarURLString == "https://example.com/new-avatar.jpg")
#expect(sub.bannerURLString == "https://example.com/new-banner.jpg")
#expect(sub.isVerified == true)
}
}
// MARK: - LocalPlaylistItem Tests
@Suite("LocalPlaylistItem")
struct LocalPlaylistItemTests {
@Test("Content source Global")
@MainActor
func contentSourceGlobal() {
let item = LocalPlaylistItem(
sortOrder: 0,
videoID: "abc123",
sourceRawValue: "global",
title: "Test",
authorName: "Channel",
authorID: "ch1",
duration: 100
)
if case .global = item.contentSource {
// Expected
} else {
Issue.record("Expected global source")
}
}
@Test("Content source Federated")
@MainActor
func contentSourceFederated() {
let item = LocalPlaylistItem(
sortOrder: 0,
videoID: "uuid123",
sourceRawValue: "federated",
instanceURLString: "https://peertube.example.com",
title: "Test",
authorName: "Channel",
authorID: "ch1",
duration: 100
)
if case .federated(_, let instance) = item.contentSource {
#expect(instance.host == "peertube.example.com")
} else {
Issue.record("Expected federated source")
}
}
@Test("Thumbnail URL conversion")
@MainActor
func thumbnailURL() {
let item = LocalPlaylistItem(
sortOrder: 0,
videoID: "abc123",
sourceRawValue: "youtube",
title: "Test",
authorName: "Channel",
authorID: "ch1",
duration: 100,
thumbnailURLString: "https://example.com/thumb.jpg"
)
#expect(item.thumbnailURL?.absoluteString == "https://example.com/thumb.jpg")
}
@Test("Default values for CloudKit compatibility")
@MainActor
func defaultValues() {
let item = LocalPlaylistItem(
sortOrder: 0,
videoID: "test",
sourceRawValue: "youtube",
title: "Test",
authorName: "Channel",
authorID: "ch1",
duration: 100
)
// Check default values are set
#expect(item.isLive == false)
#expect(item.thumbnailURLString == nil)
#expect(item.instanceURLString == nil)
#expect(item.peertubeUUID == nil)
}
}
// MARK: - DataManager Tests
@Suite("DataManager")
struct DataManagerTests {
@Test("Watch progress round trip")
@MainActor
func watchProgressRoundTrip() async throws {
let manager = try DataManager(inMemory: true)
// Create a test video
let video = Video(
id: .global("testVideo123"),
title: "Test Video",
description: nil,
author: Author(id: "ch1", name: "Channel"),
duration: 600,
publishedAt: nil,
publishedText: nil,
viewCount: nil,
likeCount: nil,
thumbnails: [],
isLive: false,
isUpcoming: false,
scheduledStartTime: nil
)
// Record progress
manager.updateWatchProgress(for: video, seconds: 300)
// Retrieve progress
let progress = manager.watchProgress(for: "testVideo123")
#expect(progress == 300)
}
@Test("Bookmark toggle")
@MainActor
func bookmarkToggle() async throws {
let manager = try DataManager(inMemory: true)
let video = Video(
id: .global("bookmarkTest"),
title: "Bookmark Me",
description: nil,
author: Author(id: "ch1", name: "Channel"),
duration: 300,
publishedAt: nil,
publishedText: nil,
viewCount: nil,
likeCount: nil,
thumbnails: [],
isLive: false,
isUpcoming: false,
scheduledStartTime: nil
)
#expect(!manager.isBookmarked(videoID: "bookmarkTest"))
manager.addBookmark(for: video)
#expect(manager.isBookmarked(videoID: "bookmarkTest"))
manager.removeBookmark(for: "bookmarkTest")
#expect(!manager.isBookmarked(videoID: "bookmarkTest"))
}
@Test("Create and delete playlist")
@MainActor
func createAndDeletePlaylist() async throws {
let manager = try DataManager(inMemory: true)
let playlist = manager.createPlaylist(title: "Test Playlist", description: "A test")
#expect(playlist.title == "Test Playlist")
#expect(playlist.playlistDescription == "A test")
var playlists = manager.playlists()
#expect(playlists.count == 1)
manager.deletePlaylist(playlist)
playlists = manager.playlists()
#expect(playlists.count == 0)
}
@Test("Subscription management")
@MainActor
func subscriptionManagement() async throws {
let manager = try DataManager(inMemory: true)
let channel = Channel(
id: .global("testChannel"),
name: "Test Channel",
description: "A test channel",
subscriberCount: 10000,
thumbnailURL: nil
)
#expect(!manager.isSubscribed(to: "testChannel"))
manager.subscribe(to: channel)
#expect(manager.isSubscribed(to: "testChannel"))
let subs = manager.subscriptions()
#expect(subs.count == 1)
#expect(subs.first?.name == "Test Channel")
manager.unsubscribe(from: "testChannel")
#expect(!manager.isSubscribed(to: "testChannel"))
}
@Test("Watch history ordering")
@MainActor
func watchHistoryOrdering() async throws {
let manager = try DataManager(inMemory: true)
// Create multiple videos
for i in 1...3 {
let video = Video(
id: .global("video\(i)"),
title: "Video \(i)",
description: nil,
author: Author(id: "ch1", name: "Channel"),
duration: 100,
publishedAt: nil,
publishedText: nil,
viewCount: nil,
likeCount: nil,
thumbnails: [],
isLive: false,
isUpcoming: false,
scheduledStartTime: nil
)
manager.updateWatchProgress(for: video, seconds: Double(i * 10))
// Small delay to ensure different timestamps
try await Task.sleep(for: .milliseconds(10))
}
let history = manager.watchHistory()
#expect(history.count == 3)
// Most recent should be first
#expect(history.first?.videoID == "video3")
}
}
}