mirror of
https://github.com/yattee/yattee.git
synced 2026-02-19 17:29:45 +00:00
352 lines
12 KiB
Swift
352 lines
12 KiB
Swift
//
|
|
// InvidiousAPIIntegrationTests.swift
|
|
// YatteeTests
|
|
//
|
|
// Integration tests for InvidiousAPI against a real instance.
|
|
// These tests make actual network requests and validate response parsing.
|
|
//
|
|
|
|
import Testing
|
|
import Foundation
|
|
@testable import Yattee
|
|
|
|
// MARK: - Integration Test Tag
|
|
|
|
extension Tag {
|
|
/// Tag for integration tests that require network access.
|
|
@Tag static var integration: Self
|
|
}
|
|
|
|
// MARK: - Invidious API Integration Tests
|
|
|
|
@Suite("Invidious API Integration Tests", .tags(.integration), .serialized)
|
|
struct InvidiousAPIIntegrationTests {
|
|
let api: InvidiousAPI
|
|
let instance: Instance
|
|
|
|
init() {
|
|
let httpClient = HTTPClient()
|
|
self.api = InvidiousAPI(httpClient: httpClient)
|
|
self.instance = IntegrationTestConstants.testInstance
|
|
}
|
|
|
|
// MARK: - Trending Tests
|
|
|
|
@Test("Trending returns videos or handles unavailable")
|
|
func trendingReturnsVideos() async throws {
|
|
do {
|
|
let videos = try await api.trending(instance: instance)
|
|
// If trending is available, it should return videos
|
|
if !videos.isEmpty {
|
|
#expect(videos.count >= 1, "Trending should return at least one video")
|
|
}
|
|
} catch {
|
|
// Trending may not be enabled on all instances - skip gracefully
|
|
// This is acceptable for integration tests
|
|
}
|
|
}
|
|
|
|
@Test("Trending videos have required fields when available")
|
|
func trendingVideosHaveRequiredFields() async throws {
|
|
do {
|
|
let videos = try await api.trending(instance: instance)
|
|
|
|
guard let video = videos.first else {
|
|
// No videos is acceptable
|
|
return
|
|
}
|
|
|
|
#expect(!video.id.videoID.isEmpty, "Video should have an ID")
|
|
#expect(!video.title.isEmpty, "Video should have a title")
|
|
#expect(!video.author.name.isEmpty, "Video should have an author name")
|
|
#expect(video.duration >= 0, "Video should have non-negative duration")
|
|
} catch {
|
|
// Trending may not be available
|
|
}
|
|
}
|
|
|
|
@Test("Trending videos have thumbnails when available")
|
|
func trendingVideosHaveThumbnails() async throws {
|
|
do {
|
|
let videos = try await api.trending(instance: instance)
|
|
|
|
guard let video = videos.first else {
|
|
return
|
|
}
|
|
|
|
#expect(!video.thumbnails.isEmpty, "Video should have thumbnails")
|
|
#expect(video.bestThumbnail != nil, "Video should have a best thumbnail")
|
|
} catch {
|
|
// Trending may not be available
|
|
}
|
|
}
|
|
|
|
// MARK: - Search Tests
|
|
|
|
@Test("Search returns results")
|
|
func searchReturnsResults() async throws {
|
|
let result = try await api.search(
|
|
query: IntegrationTestConstants.testSearchQuery,
|
|
instance: instance,
|
|
page: 1
|
|
)
|
|
|
|
#expect(!result.videos.isEmpty, "Search should return videos")
|
|
}
|
|
|
|
@Test("Search videos have required fields")
|
|
func searchVideosHaveRequiredFields() async throws {
|
|
let result = try await api.search(
|
|
query: IntegrationTestConstants.testSearchQuery,
|
|
instance: instance,
|
|
page: 1
|
|
)
|
|
|
|
guard let video = result.videos.first else {
|
|
Issue.record("No videos returned from search")
|
|
return
|
|
}
|
|
|
|
#expect(!video.id.videoID.isEmpty, "Search video should have an ID")
|
|
#expect(!video.title.isEmpty, "Search video should have a title")
|
|
}
|
|
|
|
@Test("Search pagination works")
|
|
func searchPaginationWorks() async throws {
|
|
let page1 = try await api.search(
|
|
query: IntegrationTestConstants.testSearchQuery,
|
|
instance: instance,
|
|
page: 1
|
|
)
|
|
|
|
#expect(page1.nextPage != nil, "First page should have a next page")
|
|
|
|
let page2 = try await api.search(
|
|
query: IntegrationTestConstants.testSearchQuery,
|
|
instance: instance,
|
|
page: 2
|
|
)
|
|
|
|
// Page 2 should have different videos (if enough results exist)
|
|
if !page1.videos.isEmpty && !page2.videos.isEmpty {
|
|
let page1IDs = Set(page1.videos.map { $0.id.videoID })
|
|
let page2IDs = Set(page2.videos.map { $0.id.videoID })
|
|
let overlap = page1IDs.intersection(page2IDs)
|
|
|
|
// Allow some overlap but not complete overlap
|
|
#expect(overlap.count < page1.videos.count, "Page 2 should have different videos than page 1")
|
|
}
|
|
}
|
|
|
|
@Test("Search suggestions returns strings")
|
|
func searchSuggestionsReturnsStrings() async throws {
|
|
let suggestions = try await api.searchSuggestions(
|
|
query: "never gonna",
|
|
instance: instance
|
|
)
|
|
|
|
#expect(!suggestions.isEmpty, "Search suggestions should return results")
|
|
#expect(suggestions.contains { $0.lowercased().contains("never") }, "Suggestions should be relevant to query")
|
|
}
|
|
|
|
// MARK: - Video Details Tests
|
|
|
|
@Test("Video details returns complete info")
|
|
func videoDetailsReturnsCompleteInfo() async throws {
|
|
let video = try await api.video(
|
|
id: IntegrationTestConstants.testVideoID,
|
|
instance: instance
|
|
)
|
|
|
|
#expect(video.id.videoID == IntegrationTestConstants.testVideoID, "Should return correct video")
|
|
#expect(!video.title.isEmpty, "Video should have a title")
|
|
#expect(!video.author.name.isEmpty, "Video should have an author")
|
|
#expect(video.duration > 0, "Video should have positive duration")
|
|
#expect(video.viewCount ?? 0 > 0, "Popular video should have views")
|
|
}
|
|
|
|
@Test("Video details includes thumbnails")
|
|
func videoDetailsIncludesThumbnails() async throws {
|
|
let video = try await api.video(
|
|
id: IntegrationTestConstants.testVideoID,
|
|
instance: instance
|
|
)
|
|
|
|
#expect(!video.thumbnails.isEmpty, "Video should have thumbnails")
|
|
|
|
let thumbnail = video.thumbnails.first!
|
|
#expect(thumbnail.url.absoluteString.contains("http"), "Thumbnail should have valid URL")
|
|
}
|
|
|
|
@Test("Video details includes author info")
|
|
func videoDetailsIncludesAuthorInfo() async throws {
|
|
let video = try await api.video(
|
|
id: IntegrationTestConstants.testVideoID,
|
|
instance: instance
|
|
)
|
|
|
|
#expect(!video.author.id.isEmpty, "Author should have an ID")
|
|
#expect(!video.author.name.isEmpty, "Author should have a name")
|
|
}
|
|
|
|
// MARK: - Streams Tests
|
|
|
|
@Test("Streams includes HLS when available")
|
|
func streamsIncludesHLS() async throws {
|
|
let streams = try await api.streams(
|
|
videoID: IntegrationTestConstants.testVideoID,
|
|
instance: instance
|
|
)
|
|
|
|
// HLS may not be available on all instances
|
|
let hlsStream = streams.first { $0.format == "hls" }
|
|
if let hls = hlsStream {
|
|
#expect(hls.mimeType == "application/x-mpegURL", "HLS should have correct MIME type")
|
|
}
|
|
// Test passes whether HLS is available or not
|
|
}
|
|
|
|
@Test("Streams includes multiple formats")
|
|
func streamsIncludesMultipleFormats() async throws {
|
|
let streams = try await api.streams(
|
|
videoID: IntegrationTestConstants.testVideoID,
|
|
instance: instance
|
|
)
|
|
|
|
#expect(streams.count > 1, "Should have multiple streams")
|
|
|
|
let formats = Set(streams.map { $0.format })
|
|
#expect(formats.count > 1, "Should have multiple formats")
|
|
}
|
|
|
|
@Test("Streams includes video resolutions")
|
|
func streamsIncludesVideoResolutions() async throws {
|
|
let streams = try await api.streams(
|
|
videoID: IntegrationTestConstants.testVideoID,
|
|
instance: instance
|
|
)
|
|
|
|
let videoStreams = streams.filter { $0.resolution != nil && !$0.isAudioOnly }
|
|
#expect(!videoStreams.isEmpty, "Should have video streams with resolutions")
|
|
|
|
let hasHD = videoStreams.contains { ($0.resolution?.height ?? 0) >= 720 }
|
|
#expect(hasHD, "Popular video should have HD streams")
|
|
}
|
|
|
|
@Test("Streams includes audio-only tracks")
|
|
func streamsIncludesAudioOnlyTracks() async throws {
|
|
let streams = try await api.streams(
|
|
videoID: IntegrationTestConstants.testVideoID,
|
|
instance: instance
|
|
)
|
|
|
|
let audioStreams = streams.filter { $0.isAudioOnly }
|
|
#expect(!audioStreams.isEmpty, "Should have audio-only streams")
|
|
}
|
|
|
|
// MARK: - Channel Tests
|
|
|
|
@Test("Channel returns info")
|
|
func channelReturnsInfo() async throws {
|
|
let channel = try await api.channel(
|
|
id: IntegrationTestConstants.testChannelID,
|
|
instance: instance
|
|
)
|
|
|
|
#expect(channel.id.channelID == IntegrationTestConstants.testChannelID, "Should return correct channel")
|
|
#expect(!channel.name.isEmpty, "Channel should have a name")
|
|
#expect(channel.subscriberCount ?? 0 > 0, "Popular channel should have subscribers")
|
|
}
|
|
|
|
@Test("Channel includes thumbnail")
|
|
func channelIncludesThumbnail() async throws {
|
|
let channel = try await api.channel(
|
|
id: IntegrationTestConstants.testChannelID,
|
|
instance: instance
|
|
)
|
|
|
|
#expect(channel.thumbnailURL != nil, "Channel should have thumbnail URL")
|
|
}
|
|
|
|
@Test("Channel videos returns videos")
|
|
func channelVideosReturnsVideos() async throws {
|
|
let page = try await api.channelVideos(
|
|
id: IntegrationTestConstants.testChannelID,
|
|
instance: instance,
|
|
continuation: nil
|
|
)
|
|
|
|
#expect(!page.videos.isEmpty, "Channel should have videos")
|
|
|
|
let video = page.videos.first!
|
|
#expect(!video.title.isEmpty, "Channel video should have title")
|
|
}
|
|
|
|
// MARK: - Comments Tests
|
|
|
|
@Test("Comments returns results or handles disabled")
|
|
func commentsReturnsResultsOrHandlesDisabled() async throws {
|
|
do {
|
|
let page = try await api.comments(
|
|
videoID: IntegrationTestConstants.testVideoID,
|
|
instance: instance,
|
|
continuation: nil
|
|
)
|
|
|
|
// If we get here, comments are enabled
|
|
#expect(!page.comments.isEmpty, "Video with comments should return some")
|
|
|
|
let comment = page.comments.first!
|
|
#expect(!comment.id.isEmpty, "Comment should have ID")
|
|
#expect(!comment.content.isEmpty, "Comment should have content")
|
|
#expect(!comment.author.name.isEmpty, "Comment should have author")
|
|
} catch APIError.commentsDisabled {
|
|
// Comments disabled is acceptable - test passes
|
|
} catch {
|
|
throw error
|
|
}
|
|
}
|
|
|
|
// MARK: - Captions Tests
|
|
|
|
@Test("Captions returns available tracks")
|
|
func captionsReturnsAvailableTracks() async throws {
|
|
let captions = try await api.captions(
|
|
videoID: IntegrationTestConstants.testVideoID,
|
|
instance: instance
|
|
)
|
|
|
|
// Popular video should have captions, but it's not guaranteed
|
|
if !captions.isEmpty {
|
|
let caption = captions.first!
|
|
#expect(!caption.label.isEmpty, "Caption should have label")
|
|
#expect(!caption.languageCode.isEmpty, "Caption should have language code")
|
|
#expect(caption.url.absoluteString.contains("http"), "Caption should have valid URL")
|
|
}
|
|
}
|
|
|
|
// MARK: - Error Handling Tests
|
|
|
|
@Test("Invalid video ID returns error")
|
|
func invalidVideoIDReturnsError() async throws {
|
|
do {
|
|
_ = try await api.video(id: "invalid_video_id_that_does_not_exist", instance: instance)
|
|
Issue.record("Expected error for invalid video ID")
|
|
} catch {
|
|
// Any error is acceptable - notFound, requestFailed, etc.
|
|
// Different instances may return different error codes
|
|
}
|
|
}
|
|
|
|
@Test("Invalid channel ID returns error")
|
|
func invalidChannelIDReturnsError() async throws {
|
|
do {
|
|
_ = try await api.channel(id: "invalid_channel_id_xyz", instance: instance)
|
|
Issue.record("Expected error for invalid channel ID")
|
|
} catch {
|
|
// Any error is acceptable - notFound, requestFailed, etc.
|
|
// Different instances may return different error codes
|
|
}
|
|
}
|
|
}
|