import XCTest @testable import MangaReader /// Tests unitarios para DownloadManager /// /// Estos tests deben agregarse a tu target de tests en Xcode @MainActor class DownloadManagerTests: XCTestCase { var downloadManager: DownloadManager! var storage: StorageService! override func setUp() async throws { downloadManager = DownloadManager.shared storage = StorageService.shared // Limpiar estado antes de cada test downloadManager.cancelAllDownloads() downloadManager.clearCompletedHistory() downloadManager.clearFailedHistory() } override func tearDown() async throws { // Limpiar estado después de cada test downloadManager.cancelAllDownloads() } // MARK: - Test: Descarga Individual func testDownloadSingleChapter() async throws { // Given let mangaSlug = "test-manga" let mangaTitle = "Test Manga" let chapter = Chapter( number: 1, title: "Chapter 1", url: "https://example.com/chapter1", slug: "chapter-1" ) // When try await downloadManager.downloadChapter( mangaSlug: mangaSlug, mangaTitle: mangaTitle, chapter: chapter ) // Then XCTAssertEqual(downloadManager.activeDownloads.count, 0, "No debe haber descargas activas") XCTAssertEqual(downloadManager.completedDownloads.count, 1, "Debe haber una descarga completada") XCTAssertTrue(storage.isChapterDownloaded(mangaSlug: mangaSlug, chapterNumber: 1), "El capítulo debe estar descargado") } func testDownloadAlreadyDownloadedChapter() async throws { // Given let mangaSlug = "test-manga" let mangaTitle = "Test Manga" let chapter = Chapter( number: 1, title: "Chapter 1", url: "https://example.com/chapter1", slug: "chapter-1" ) // Descargar por primera vez try await downloadManager.downloadChapter( mangaSlug: mangaSlug, mangaTitle: mangaTitle, chapter: chapter ) // When & Then - Intentar descargar nuevamente do { try await downloadManager.downloadChapter( mangaSlug: mangaSlug, mangaTitle: mangaTitle, chapter: chapter ) XCTFail("Debe lanzar error alreadyDownloaded") } catch DownloadError.alreadyDownloaded { // Éxito - Error esperado } catch { XCTFail("Error incorrecto: \(error)") } } // MARK: - Test: Descarga Múltiple func testDownloadMultipleChapters() async throws { // Given let mangaSlug = "test-manga" let mangaTitle = "Test Manga" let chapters = [ Chapter(number: 1, title: "Chapter 1", url: "https://example.com/ch1", slug: "ch1"), Chapter(number: 2, title: "Chapter 2", url: "https://example.com/ch2", slug: "ch2"), Chapter(number: 3, title: "Chapter 3", url: "https://example.com/ch3", slug: "ch3") ] // When await downloadManager.downloadChapters( mangaSlug: mangaSlug, mangaTitle: mangaTitle, chapters: chapters ) // Esperar a que todas terminen try await Task.sleep(nanoseconds: 10_000_000_000) // 10 segundos // Then XCTAssertEqual(downloadManager.completedDownloads.count, 3, "Debe haber 3 descargas completadas") } // MARK: - Test: Cancelación func testCancelDownload() async throws { // Given let mangaSlug = "test-manga" let mangaTitle = "Test Manga" let chapter = Chapter( number: 1, title: "Chapter 1", url: "https://example.com/chapter1", slug: "chapter-1" ) // Iniciar descarga en background Task { try? await downloadManager.downloadChapter( mangaSlug: mangaSlug, mangaTitle: mangaTitle, chapter: chapter ) } // Esperar un poco para que inicie try await Task.sleep(nanoseconds: 500_000_000) // 0.5 segundos // When guard let task = downloadManager.activeDownloads.first else { XCTFail("Debe haber una descarga activa") return } downloadManager.cancelDownload(taskId: task.id) // Then XCTAssertEqual(downloadManager.activeDownloads.count, 0, "No debe haber descargas activas") XCTAssertFalse(storage.isChapterDownloaded(mangaSlug: mangaSlug, chapterNumber: 1), "El capítulo no debe estar descargado") } func testCancelAllDownloads() async throws { // Given let mangaSlug = "test-manga" let mangaTitle = "Test Manga" let chapters = [ Chapter(number: 1, title: "Chapter 1", url: "https://example.com/ch1", slug: "ch1"), Chapter(number: 2, title: "Chapter 2", url: "https://example.com/ch2", slug: "ch2"), Chapter(number: 3, title: "Chapter 3", url: "https://example.com/ch3", slug: "ch3") ] // Iniciar descargas Task { await downloadManager.downloadChapters( mangaSlug: mangaSlug, mangaTitle: mangaTitle, chapters: chapters ) } // Esperar un poco try await Task.sleep(nanoseconds: 500_000_000) // 0.5 segundos // When downloadManager.cancelAllDownloads() // Then XCTAssertEqual(downloadManager.activeDownloads.count, 0, "No debe haber descargas activas") } // MARK: - Test: Progreso func testDownloadProgress() async throws { // Given let mangaSlug = "test-manga" let mangaTitle = "Test Manga" let chapter = Chapter( number: 1, title: "Chapter 1", url: "https://example.com/chapter1", slug: "chapter-1" ) // Expectation para progreso let progressExpectation = expectation(description: "Progreso actualizado") // Observer de progreso let cancellable = downloadManager.$activeDownloads.sink { tasks in if let task = tasks.first { if task.progress > 0 && task.progress < 1 { progressExpectation.fulfill() } } } // When Task { try? await downloadManager.downloadChapter( mangaSlug: mangaSlug, mangaTitle: mangaTitle, chapter: chapter ) } // Then await fulfillment(of: [progressExpectation], timeout: 5.0) cancellable.cancel() } // MARK: - Test: Errores func testDownloadWithInvalidURL() async throws { // Given let mangaSlug = "test-manga" let mangaTitle = "Test Manga" let chapter = Chapter( number: 1, title: "Chapter 1", url: "invalid-url", slug: "invalid-chapter" ) // When & Then do { try await downloadManager.downloadChapter( mangaSlug: mangaSlug, mangaTitle: mangaTitle, chapter: chapter ) XCTFail("Debe lanzar error") } catch { // Éxito - Se espera un error XCTAssertNotNil(error, "Debe haber un error") } } // MARK: - Test: Concurrencia func testMaxConcurrentDownloads() async throws { // Given let mangaSlug = "test-manga" let mangaTitle = "Test Manga" let chapters = (1...10).map { i in Chapter(number: i, title: "Chapter \(i)", url: "https://example.com/ch\(i)", slug: "ch\(i)") } // When Task { await downloadManager.downloadChapters( mangaSlug: mangaSlug, mangaTitle: mangaTitle, chapters: chapters ) } // Esperar un poco try await Task.sleep(nanoseconds: 1_000_000_000) // 1 segundo // Then - No debe exceder el máximo de descargas concurrentes XCTAssertLessThanOrEqual( downloadManager.activeDownloads.count, 3, "No debe haber más de 3 descargas activas simultáneas" ) } // MARK: - Test: Storage func testStorageIntegration() async throws { // Given let mangaSlug = "test-manga" let mangaTitle = "Test Manga" let chapter = Chapter( number: 1, title: "Chapter 1", url: "https://example.com/chapter1", slug: "chapter-1" ) // When try await downloadManager.downloadChapter( mangaSlug: mangaSlug, mangaTitle: mangaTitle, chapter: chapter ) // Then - Verificar integración con StorageService XCTAssertTrue( storage.isChapterDownloaded(mangaSlug: mangaSlug, chapterNumber: 1), "StorageService debe reportar el capítulo como descargado" ) XCTAssertNotNil( storage.getDownloadedChapter(mangaSlug: mangaSlug, chapterNumber: 1), "StorageService debe retornar metadata del capítulo" ) let chapterDir = storage.getChapterDirectory(mangaSlug: mangaSlug, chapterNumber: 1) XCTAssertTrue( FileManager.default.fileExists(atPath: chapterDir.path), "El directorio del capítulo debe existir" ) } func testClearStorage() async throws { // Given let mangaSlug = "test-manga" let mangaTitle = "Test Manga" let chapter = Chapter( number: 1, title: "Chapter 1", url: "https://example.com/chapter1", slug: "chapter-1" ) // Descargar capítulo try await downloadManager.downloadChapter( mangaSlug: mangaSlug, mangaTitle: mangaTitle, chapter: chapter ) // When storage.deleteDownloadedChapter(mangaSlug: mangaSlug, chapterNumber: 1) // Then XCTAssertFalse( storage.isChapterDownloaded(mangaSlug: mangaSlug, chapterNumber: 1), "El capítulo no debe estar descargado después de eliminarlo" ) XCTAssertEqual( storage.getStorageSize(), 0, "El tamaño de almacenamiento debe ser 0" ) } // MARK: - Test: Estadísticas func testDownloadStats() async throws { // Given let initialStats = downloadManager.downloadStats // When let mangaSlug = "test-manga" let mangaTitle = "Test Manga" let chapter = Chapter( number: 1, title: "Chapter 1", url: "https://example.com/chapter1", slug: "chapter-1" ) try await downloadManager.downloadChapter( mangaSlug: mangaSlug, mangaTitle: mangaTitle, chapter: chapter ) let finalStats = downloadManager.downloadStats // Then XCTAssertEqual( finalStats.completedDownloads, initialStats.completedDownloads + 1, "Las descargas completadas deben incrementar en 1" ) } // MARK: - Test: Historiales func testClearCompletedHistory() async throws { // Given let mangaSlug = "test-manga" let mangaTitle = "Test Manga" let chapter = Chapter( number: 1, title: "Chapter 1", url: "https://example.com/chapter1", slug: "chapter-1" ) try await downloadManager.downloadChapter( mangaSlug: mangaSlug, mangaTitle: mangaTitle, chapter: chapter ) // When downloadManager.clearCompletedHistory() // Then XCTAssertEqual( downloadManager.completedDownloads.count, 0, "El historial de completadas debe estar vacío" ) } } /// Tests para DownloadTask @MainActor class DownloadTaskTests: XCTestCase { func testDownloadTaskInitialization() { // Given let task = DownloadTask( mangaSlug: "test-manga", mangaTitle: "Test Manga", chapterNumber: 1, chapterTitle: "Chapter 1", imageURLs: ["url1", "url2", "url3"] ) // Then XCTAssertEqual(task.mangaSlug, "test-manga") XCTAssertEqual(task.chapterNumber, 1) XCTAssertEqual(task.imageURLs.count, 3) XCTAssertEqual(task.downloadedPages, 0) XCTAssertEqual(task.progress, 0.0) } func testDownloadTaskProgress() { // Given let task = DownloadTask( mangaSlug: "test-manga", mangaTitle: "Test Manga", chapterNumber: 1, chapterTitle: "Chapter 1", imageURLs: ["url1", "url2", "url3", "url4", "url5"] ) // When task.updateProgress(downloaded: 2, total: 5) // Then XCTAssertEqual(task.downloadedPages, 2) XCTAssertEqual(task.progress, 0.4, accuracy: 0.01) } func testDownloadTaskCompletion() { // Given let task = DownloadTask( mangaSlug: "test-manga", mangaTitle: "Test Manga", chapterNumber: 1, chapterTitle: "Chapter 1", imageURLs: ["url1", "url2", "url3"] ) // When task.complete() // Then XCTAssertTrue(task.state.isCompleted) } func testDownloadTaskCancellation() { // Given let task = DownloadTask( mangaSlug: "test-manga", mangaTitle: "Test Manga", chapterNumber: 1, chapterTitle: "Chapter 1", imageURLs: ["url1", "url2", "url3"] ) // When task.cancel() // Then XCTAssertTrue(task.isCancelled) XCTAssertTrue(task.state.isTerminal) } } /// Tests para Extensions class DownloadExtensionsTests: XCTestCase { func testDownloadTaskFormattedSize() { // Given let task = DownloadTask( mangaSlug: "test-manga", mangaTitle: "Test Manga", chapterNumber: 1, chapterTitle: "Chapter 1", imageURLs: Array(repeating: "url", count: 10) ) // When let size = task.formattedSize // Then XCTAssertFalse(size.isEmpty, "El tamaño formateado no debe estar vacío") } func testUIImageOptimization() { // Given let imageSize = CGSize(width: 3000, height: 4000) UIGraphicsBeginImageContextWithOptions(imageSize, false, 1.0) let context = UIGraphicsGetCurrentContext() context?.setFillColor(UIColor.blue.cgColor) context?.fill(CGRect(origin: .zero, size: imageSize)) let largeImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() guard let image = largeImage else { XCTFail("No se pudo crear la imagen de prueba") return } // When let optimizedData = image.optimizedForStorage() // Then XCTAssertNotNil(optimizedData, "Debe generar datos optimizados") XCTAssertTrue(optimizedData!.count > 0, "Los datos no deben estar vacíos") } }