Skip to content

Commit

Permalink
test: add unit tests for DownloadManager (#532)
Browse files Browse the repository at this point in the history
* test: add DownloadManager tests

* fix: delete old file

* fix: add SwiftyMocky to DownloadManager tests

* fix: address feedback

* fix: updated mock files
  • Loading branch information
IvanStepanok authored Oct 29, 2024
1 parent 8cfe33f commit c4fcc19
Show file tree
Hide file tree
Showing 18 changed files with 6,439 additions and 183 deletions.
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

0 comments on commit c4fcc19

Please sign in to comment.