Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: add unit tests for DownloadManager #532

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ opt_in_rules: # some rules are only opt-in
excluded: # paths to ignore during linting. Takes precedence over `included`.
- Carthage
- Pods
- Core/CoreTests
- Authorization/AuthorizationTests
- Course/CourseTests
- Dashboard/DashboardTests
Expand Down
4 changes: 2 additions & 2 deletions Authorization/Authorization/SwiftGen/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ public enum AuthLocalization {
public static let ssoHeading = AuthLocalization.tr("Localizable", "SIGN_IN.SSO_HEADING", fallback: "Start today to build your career with confidence")
/// Log in through the national unified sign-on service
public static let ssoLogInSubtitle = AuthLocalization.tr("Localizable", "SIGN_IN.SSO_LOG_IN_SUBTITLE", fallback: "Log in through the national unified sign-on service")
/// Sign IN
public static let ssoLogInTitle = AuthLocalization.tr("Localizable", "SIGN_IN.SSO_LOG_IN_TITLE", fallback: "Sign IN")
/// Sign in
public static let ssoLogInTitle = AuthLocalization.tr("Localizable", "SIGN_IN.SSO_LOG_IN_TITLE", fallback: "Sign in")
/// An integrated set of knowledge and empowerment programs to develop the components of the endowment sector and its workers
public static let ssoSupportingText = AuthLocalization.tr("Localizable", "SIGN_IN.SSO_SUPPORTING_TEXT", fallback: "An integrated set of knowledge and empowerment programs to develop the components of the endowment sector and its workers")
/// Welcome back! Sign in to access your courses.
Expand Down
101 changes: 101 additions & 0 deletions Core/Core.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Core/Core/Data/CoreStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import Foundation

//sourcery: AutoMockable
public protocol CoreStorage {
var accessToken: String? {get set}
var refreshToken: String? {get set}
Expand Down
4 changes: 2 additions & 2 deletions Core/Core/Network/DownloadManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ public class DownloadManager: DownloadManagerProtocol {
return updatedSequentials
}

private func calculateFolderSize(at url: URL) throws -> Int {
func calculateFolderSize(at url: URL) throws -> Int {
let fileManager = FileManager.default
let resourceKeys: [URLResourceKey] = [.isDirectoryKey, .fileSizeKey]
var totalSize: Int64 = 0
Expand Down Expand Up @@ -705,7 +705,7 @@ public class DownloadManager: DownloadManagerProtocol {
}
}

private func isMD5Hash(_ folderName: String) -> Bool {
func isMD5Hash(_ folderName: String) -> Bool {
let md5Regex = "^[a-fA-F0-9]{32}$"
let predicate = NSPredicate(format: "SELF MATCHES %@", md5Regex)
return predicate.evaluate(with: folderName)
Expand Down
3,641 changes: 3,641 additions & 0 deletions Core/CoreTests/CoreMock.generated.swift

Large diffs are not rendered by default.

328 changes: 328 additions & 0 deletions Core/CoreTests/DownloadManager/DownloadManagerTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,328 @@
//
// DownloadManagerTests.swift
// Core
//
// Created by Ivan Stepanok on 22.10.2024.
//

import XCTest
import SwiftyMocky
@testable import Core

final class DownloadManagerTests: XCTestCase {

var persistence: CorePersistenceProtocolMock!
var storage: CoreStorageMock!
var connectivity: ConnectivityProtocolMock!

override func setUp() {
super.setUp()
persistence = CorePersistenceProtocolMock()
storage = CoreStorageMock()
connectivity = ConnectivityProtocolMock()
}

// MARK: - Test Add to Queue

func testAddToDownloadQueue_WhenWiFiOnlyAndOnWiFi_ShouldAddToQueue() async throws {
// Given
Given(connectivity, .isInternetAvaliable(getter: true))
Given(connectivity, .internetReachableSubject(getter: .init(.reachable)))
Given(connectivity, .isMobileData(getter: false))

let downloadManager = DownloadManager(
persistence: persistence,
appStorage: storage,
connectivity: connectivity
)

Given(storage, .userSettings(getter: UserSettings(
wifiOnly: true,
streamingQuality: .auto,
downloadQuality: .auto
)))

let blocks = [createMockCourseBlock()]

// When
try await downloadManager.addToDownloadQueue(blocks: blocks)

// Then
Verify(persistence, 1, .addToDownloadQueue(blocks: .value(blocks), downloadQuality: .value(.auto)))
}

func testAddToDownloadQueue_WhenWiFiOnlyAndOnMobileData_ShouldThrowError() async {
// Given
Given(storage, .userSettings(getter: UserSettings(
wifiOnly: true,
streamingQuality: .auto,
downloadQuality: .auto
)))
Given(connectivity, .isInternetAvaliable(getter: true))
Given(connectivity, .isMobileData(getter: true))

let downloadManager = DownloadManager(
persistence: persistence,
appStorage: storage,
connectivity: connectivity
)

let blocks = [createMockCourseBlock()]

// When/Then
do {
try await downloadManager.addToDownloadQueue(blocks: blocks)
XCTFail("Should throw NoWiFiError")
} catch is NoWiFiError {
// Success
Verify(persistence, 0, .addToDownloadQueue(blocks: .any, downloadQuality: .value(.auto)))
} catch {
XCTFail("Unexpected error: \(error)")
}
}

// MARK: - Test New Download

func testNewDownload_WhenTaskAvailable_ShouldStartDownloading() async throws {
// Given
let mockTask = createMockDownloadTask()
Given(persistence, .getDownloadDataTasks(willReturn: [mockTask]))
Given(persistence, .nextBlockForDownloading(willReturn: mockTask))
Given(connectivity, .isInternetAvaliable(getter: true))
Given(connectivity, .isMobileData(getter: false))

let downloadManager = DownloadManager(
persistence: persistence,
appStorage: storage,
connectivity: connectivity
)

// When
try await downloadManager.resumeDownloading()

// Then
Verify(persistence, 2, .nextBlockForDownloading())
XCTAssertEqual(downloadManager.currentDownloadTask?.id, mockTask.id)
}

// MARK: - Test Cancel Downloads

func testCancelDownloading_ForSpecificTask_ShouldRemoveFileAndTask() async throws {
// Given
let task = createMockDownloadTask()
Given(connectivity, .isInternetAvaliable(getter: true))
Given(connectivity, .isMobileData(getter: false))
Given(persistence, .deleteDownloadDataTask(id: .value(task.id), willProduce: { _ in }))

let downloadManager = DownloadManager(
persistence: persistence,
appStorage: storage,
connectivity: connectivity
)

// When
try await downloadManager.cancelDownloading(task: task)

// Then
Verify(persistence, 1, .deleteDownloadDataTask(id: .value(task.id)))
}

func testCancelDownloading_ForCourse_ShouldCancelAllTasksForCourse() async throws {
// Given
let courseId = "course123"
let task = createMockDownloadTask(courseId: courseId)
let tasks = [task]

Given(connectivity, .isInternetAvaliable(getter: true))
Given(connectivity, .isMobileData(getter: false))
Given(persistence, .getDownloadDataTasksForCourse(.value(courseId), willReturn: tasks))
Given(persistence, .deleteDownloadDataTask(id: .value(task.id), willProduce: { _ in }))

let downloadManager = DownloadManager(
persistence: persistence,
appStorage: storage,
connectivity: connectivity
)

// When
try await downloadManager.cancelDownloading(courseId: courseId)

// Then
Verify(persistence, 1, .getDownloadDataTasksForCourse(.value(courseId)))
Verify(persistence, 1, .deleteDownloadDataTask(id: .value(task.id)))
}

// MARK: - Test File Management

func testDeleteFile_ShouldRemoveFileAndTask() async {
// Given
let block = createMockCourseBlock()
Given(connectivity, .isInternetAvaliable(getter: true))
Given(connectivity, .isMobileData(getter: false))
Given(persistence, .deleteDownloadDataTask(id: .value(block.id), willProduce: { _ in }))

let downloadManager = DownloadManager(
persistence: persistence,
appStorage: storage,
connectivity: connectivity
)

// When
await downloadManager.deleteFile(blocks: [block])

// Then
Verify(persistence, 1, .deleteDownloadDataTask(id: .value(block.id)))
}

func testFileUrl_ForFinishedTask_ShouldReturnCorrectUrl() {
// Given
let task = createMockDownloadTask(state: .finished)
let mockUser = DataLayer.User(
id: 1,
username: "test",
email: "[email protected]",
name: "Test User"
)

Given(storage, .user(getter: mockUser))
Given(connectivity, .isInternetAvaliable(getter: true))
Given(connectivity, .isMobileData(getter: false))
Given(persistence, .downloadDataTask(for: .value(task.id), willReturn: task))

let downloadManager = DownloadManager(
persistence: persistence,
appStorage: storage,
connectivity: connectivity
)

// When
let url = downloadManager.fileUrl(for: task.id)

// Then
XCTAssertNotNil(url)
Verify(persistence, 1, .downloadDataTask(for: .value(task.id)))
XCTAssertEqual(url?.lastPathComponent, task.fileName)
}

// MARK: - Test Video Size Calculation

func testIsLargeVideosSize_WhenOver1GB_ShouldReturnTrue() {
// Given
let blocks = [createMockCourseBlock(videoSize: 1_200_000_000)] // 1.2 GB
Given(connectivity, .isInternetAvaliable(getter: true))
Given(connectivity, .isMobileData(getter: false))

let downloadManager = DownloadManager(
persistence: persistence,
appStorage: storage,
connectivity: connectivity
)

// When
let isLarge = downloadManager.isLargeVideosSize(blocks: blocks)

// Then
XCTAssertTrue(isLarge)
}

func testIsLargeVideosSize_WhenUnder1GB_ShouldReturnFalse() {
// Given
let blocks = [createMockCourseBlock(videoSize: 500_000_000)] // 500 MB
Given(connectivity, .isInternetAvaliable(getter: true))
Given(connectivity, .isMobileData(getter: false))

let downloadManager = DownloadManager(
persistence: persistence,
appStorage: storage,
connectivity: connectivity
)

// When
let isLarge = downloadManager.isLargeVideosSize(blocks: blocks)

// Then
XCTAssertFalse(isLarge)
}

// MARK: - Test Download Tasks Retrieval

func testGetDownloadTasks_ShouldReturnAllTasks() async {
// Given
let expectedTasks = [
createMockDownloadTask(id: "1"),
createMockDownloadTask(id: "2")
]

Given(connectivity, .isInternetAvaliable(getter: true))
Given(connectivity, .isMobileData(getter: false))
Given(persistence, .getDownloadDataTasks(willReturn: expectedTasks))

let downloadManager = DownloadManager(
persistence: persistence,
appStorage: storage,
connectivity: connectivity
)

// When
let tasks = await downloadManager.getDownloadTasks()

// Then
Verify(persistence, 1, .getDownloadDataTasks())
XCTAssertEqual(tasks.count, expectedTasks.count)
XCTAssertEqual(tasks[0].id, expectedTasks[0].id)
XCTAssertEqual(tasks[1].id, expectedTasks[1].id)
}

// MARK: - Helper Methods

private func createMockDownloadTask(
id: String = "test123",
courseId: String = "course123",
state: DownloadState = .waiting
) -> DownloadDataTask {
DownloadDataTask(
id: id,
blockId: "block123",
courseId: courseId,
userId: 1,
url: "https://test.com/video.mp4",
fileName: "video.mp4",
displayName: "Test Video",
progress: 0,
resumeData: nil,
state: state,
type: .video,
fileSize: 1000,
lastModified: "2024-01-01"
)
}

private func createMockCourseBlock(videoSize: Int = 1000) -> CourseBlock {
CourseBlock(
blockId: "block123",
id: "test123",
courseId: "course123",
graded: false,
due: nil,
completion: 0,
type: .video,
displayName: "Test Video",
studentUrl: "https://test.com",
webUrl: "https://test.com",
encodedVideo: CourseBlockEncodedVideo(
fallback: CourseBlockVideo(
url: "https://test.com/video.mp4",
fileSize: videoSize,
streamPriority: 1
),
youtube: nil,
desktopMP4: nil,
mobileHigh: nil,
mobileLow: nil,
hls: nil
),
multiDevice: true,
offlineDownload: nil
)
}
}
16 changes: 16 additions & 0 deletions Core/Mockfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
sourceryCommand: mint run krzysztofzablocki/[email protected] sourcery
sourceryTemplate: ../MockTemplate.swifttemplate
unit.tests.mock:
sources:
include:
- ./../Core
- ./Core
exclude: []
output: ./CoreTests/CoreMock.generated.swift
targets:
- MyAppUnitTests
import:
- Core
- Foundation
- SwiftUI
- Combine
Loading
Loading