diff --git a/Authorization/AuthorizationTests/AuthorizationMock.generated.swift b/Authorization/AuthorizationTests/AuthorizationMock.generated.swift index fbbb73a12..9bf9f87dc 100644 --- a/Authorization/AuthorizationTests/AuthorizationMock.generated.swift +++ b/Authorization/AuthorizationTests/AuthorizationMock.generated.swift @@ -1931,6 +1931,19 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { return __value } + open func addToDownloadQueue(blocks: [CourseBlock]) throws { + addInvocation(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) + let perform = methodPerformValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void + perform?(`blocks`) + do { + _ = try methodReturnValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + open func getDownloadTasks() -> [DownloadDataTask] { addInvocation(.m_getDownloadTasks) let perform = methodPerformValue(.m_getDownloadTasks) as? () -> Void @@ -1998,6 +2011,19 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { } } + open func cancelAllDownloading() throws { + addInvocation(.m_cancelAllDownloading) + let perform = methodPerformValue(.m_cancelAllDownloading) as? () -> Void + perform?() + do { + _ = try methodReturnValue(.m_cancelAllDownloading).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + open func deleteFile(blocks: [CourseBlock]) { addInvocation(.m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) let perform = methodPerformValue(.m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void @@ -2023,12 +2049,12 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { return __value } - open func addToDownloadQueue(blocks: [CourseBlock]) throws { - addInvocation(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) - let perform = methodPerformValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void - perform?(`blocks`) + open func resumeDownloading() throws { + addInvocation(.m_resumeDownloading) + let perform = methodPerformValue(.m_resumeDownloading) as? () -> Void + perform?() do { - _ = try methodReturnValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))).casted() as Void + _ = try methodReturnValue(.m_resumeDownloading).casted() as Void } catch MockError.notStubed { // do nothing } catch { @@ -2050,34 +2076,22 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { return __value } - open func resumeDownloading() throws { - addInvocation(.m_resumeDownloading) - let perform = methodPerformValue(.m_resumeDownloading) as? () -> Void - perform?() - do { - _ = try methodReturnValue(.m_resumeDownloading).casted() as Void - } catch MockError.notStubed { - // do nothing - } catch { - throw error - } - } - fileprivate enum MethodType { case m_publisher case m_eventPublisher + case m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>) case m_getDownloadTasks case m_getDownloadTasksForCourse__courseId(Parameter) case m_cancelDownloading__courseId_courseIdblocks_blocks(Parameter, Parameter<[CourseBlock]>) case m_cancelDownloading__task_task(Parameter) case m_cancelDownloading__courseId_courseId(Parameter) + case m_cancelAllDownloading case m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>) case m_deleteAllFiles case m_fileUrl__for_blockId(Parameter) - case m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>) - case m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>) case m_resumeDownloading + case m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>) case p_currentDownloadTask_get static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { @@ -2086,6 +2100,11 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { case (.m_eventPublisher, .m_eventPublisher): return .match + case (.m_addToDownloadQueue__blocks_blocks(let lhsBlocks), .m_addToDownloadQueue__blocks_blocks(let rhsBlocks)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) + return Matcher.ComparisonResult(results) + case (.m_getDownloadTasks, .m_getDownloadTasks): return .match case (.m_getDownloadTasksForCourse__courseId(let lhsCourseid), .m_getDownloadTasksForCourse__courseId(let rhsCourseid)): @@ -2109,6 +2128,8 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) return Matcher.ComparisonResult(results) + case (.m_cancelAllDownloading, .m_cancelAllDownloading): return .match + case (.m_deleteFile__blocks_blocks(let lhsBlocks), .m_deleteFile__blocks_blocks(let rhsBlocks)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) @@ -2121,17 +2142,12 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockId")) return Matcher.ComparisonResult(results) - case (.m_addToDownloadQueue__blocks_blocks(let lhsBlocks), .m_addToDownloadQueue__blocks_blocks(let rhsBlocks)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) - return Matcher.ComparisonResult(results) + case (.m_resumeDownloading, .m_resumeDownloading): return .match case (.m_isLargeVideosSize__blocks_blocks(let lhsBlocks), .m_isLargeVideosSize__blocks_blocks(let rhsBlocks)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) return Matcher.ComparisonResult(results) - - case (.m_resumeDownloading, .m_resumeDownloading): return .match case (.p_currentDownloadTask_get,.p_currentDownloadTask_get): return Matcher.ComparisonResult.match default: return .none } @@ -2141,17 +2157,18 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { switch self { case .m_publisher: return 0 case .m_eventPublisher: return 0 + case let .m_addToDownloadQueue__blocks_blocks(p0): return p0.intValue case .m_getDownloadTasks: return 0 case let .m_getDownloadTasksForCourse__courseId(p0): return p0.intValue case let .m_cancelDownloading__courseId_courseIdblocks_blocks(p0, p1): return p0.intValue + p1.intValue case let .m_cancelDownloading__task_task(p0): return p0.intValue case let .m_cancelDownloading__courseId_courseId(p0): return p0.intValue + case .m_cancelAllDownloading: return 0 case let .m_deleteFile__blocks_blocks(p0): return p0.intValue case .m_deleteAllFiles: return 0 case let .m_fileUrl__for_blockId(p0): return p0.intValue - case let .m_addToDownloadQueue__blocks_blocks(p0): return p0.intValue - case let .m_isLargeVideosSize__blocks_blocks(p0): return p0.intValue case .m_resumeDownloading: return 0 + case let .m_isLargeVideosSize__blocks_blocks(p0): return p0.intValue case .p_currentDownloadTask_get: return 0 } } @@ -2159,17 +2176,18 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { switch self { case .m_publisher: return ".publisher()" case .m_eventPublisher: return ".eventPublisher()" + case .m_addToDownloadQueue__blocks_blocks: return ".addToDownloadQueue(blocks:)" case .m_getDownloadTasks: return ".getDownloadTasks()" case .m_getDownloadTasksForCourse__courseId: return ".getDownloadTasksForCourse(_:)" case .m_cancelDownloading__courseId_courseIdblocks_blocks: return ".cancelDownloading(courseId:blocks:)" case .m_cancelDownloading__task_task: return ".cancelDownloading(task:)" case .m_cancelDownloading__courseId_courseId: return ".cancelDownloading(courseId:)" + case .m_cancelAllDownloading: return ".cancelAllDownloading()" case .m_deleteFile__blocks_blocks: return ".deleteFile(blocks:)" case .m_deleteAllFiles: return ".deleteAllFiles()" case .m_fileUrl__for_blockId: return ".fileUrl(for:)" - case .m_addToDownloadQueue__blocks_blocks: return ".addToDownloadQueue(blocks:)" - case .m_isLargeVideosSize__blocks_blocks: return ".isLargeVideosSize(blocks:)" case .m_resumeDownloading: return ".resumeDownloading()" + case .m_isLargeVideosSize__blocks_blocks: return ".isLargeVideosSize(blocks:)" case .p_currentDownloadTask_get: return "[get] .currentDownloadTask" } } @@ -2247,6 +2265,16 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { willProduce(stubber) return given } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willThrow: Error...) -> MethodStub { + return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } public static func cancelDownloading(courseId: Parameter, blocks: Parameter<[CourseBlock]>, willThrow: Error...) -> MethodStub { return Given(method: .m_cancelDownloading__courseId_courseIdblocks_blocks(`courseId`, `blocks`), products: willThrow.map({ StubProduct.throw($0) })) } @@ -2277,12 +2305,12 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { willProduce(stubber) return given } - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willThrow: Error...) -> MethodStub { - return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) + public static func cancelAllDownloading(willThrow: Error...) -> MethodStub { + return Given(method: .m_cancelAllDownloading, products: willThrow.map({ StubProduct.throw($0) })) } - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willProduce: (StubberThrows) -> Void) -> MethodStub { + public static func cancelAllDownloading(willProduce: (StubberThrows) -> Void) -> MethodStub { let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) }() + let given: Given = { return Given(method: .m_cancelAllDownloading, products: willThrow.map({ StubProduct.throw($0) })) }() let stubber = given.stubThrows(for: (Void).self) willProduce(stubber) return given @@ -2304,17 +2332,18 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func publisher() -> Verify { return Verify(method: .m_publisher)} public static func eventPublisher() -> Verify { return Verify(method: .m_eventPublisher)} + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_addToDownloadQueue__blocks_blocks(`blocks`))} public static func getDownloadTasks() -> Verify { return Verify(method: .m_getDownloadTasks)} public static func getDownloadTasksForCourse(_ courseId: Parameter) -> Verify { return Verify(method: .m_getDownloadTasksForCourse__courseId(`courseId`))} public static func cancelDownloading(courseId: Parameter, blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_cancelDownloading__courseId_courseIdblocks_blocks(`courseId`, `blocks`))} public static func cancelDownloading(task: Parameter) -> Verify { return Verify(method: .m_cancelDownloading__task_task(`task`))} public static func cancelDownloading(courseId: Parameter) -> Verify { return Verify(method: .m_cancelDownloading__courseId_courseId(`courseId`))} + public static func cancelAllDownloading() -> Verify { return Verify(method: .m_cancelAllDownloading)} public static func deleteFile(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_deleteFile__blocks_blocks(`blocks`))} public static func deleteAllFiles() -> Verify { return Verify(method: .m_deleteAllFiles)} public static func fileUrl(for blockId: Parameter) -> Verify { return Verify(method: .m_fileUrl__for_blockId(`blockId`))} - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_addToDownloadQueue__blocks_blocks(`blocks`))} - public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_isLargeVideosSize__blocks_blocks(`blocks`))} public static func resumeDownloading() -> Verify { return Verify(method: .m_resumeDownloading)} + public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_isLargeVideosSize__blocks_blocks(`blocks`))} public static var currentDownloadTask: Verify { return Verify(method: .p_currentDownloadTask_get) } } @@ -2328,6 +2357,9 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func eventPublisher(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_eventPublisher, performs: perform) } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { + return Perform(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), performs: perform) + } public static func getDownloadTasks(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_getDownloadTasks, performs: perform) } @@ -2343,6 +2375,9 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func cancelDownloading(courseId: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_cancelDownloading__courseId_courseId(`courseId`), performs: perform) } + public static func cancelAllDownloading(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_cancelAllDownloading, performs: perform) + } public static func deleteFile(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { return Perform(method: .m_deleteFile__blocks_blocks(`blocks`), performs: perform) } @@ -2352,14 +2387,257 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func fileUrl(for blockId: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_fileUrl__for_blockId(`blockId`), performs: perform) } - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { - return Perform(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), performs: perform) + public static func resumeDownloading(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_resumeDownloading, performs: perform) } public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { return Perform(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), performs: perform) } - public static func resumeDownloading(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_resumeDownloading, performs: perform) + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - WebviewCookiesUpdateProtocol + +open class WebviewCookiesUpdateProtocolMock: WebviewCookiesUpdateProtocol, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + public var authInteractor: AuthInteractorProtocol { + get { invocations.append(.p_authInteractor_get); return __p_authInteractor ?? givenGetterValue(.p_authInteractor_get, "WebviewCookiesUpdateProtocolMock - stub value for authInteractor was not defined") } + } + private var __p_authInteractor: (AuthInteractorProtocol)? + + public var cookiesReady: Bool { + get { invocations.append(.p_cookiesReady_get); return __p_cookiesReady ?? givenGetterValue(.p_cookiesReady_get, "WebviewCookiesUpdateProtocolMock - stub value for cookiesReady was not defined") } + set { invocations.append(.p_cookiesReady_set(.value(newValue))); __p_cookiesReady = newValue } + } + private var __p_cookiesReady: (Bool)? + + public var updatingCookies: Bool { + get { invocations.append(.p_updatingCookies_get); return __p_updatingCookies ?? givenGetterValue(.p_updatingCookies_get, "WebviewCookiesUpdateProtocolMock - stub value for updatingCookies was not defined") } + set { invocations.append(.p_updatingCookies_set(.value(newValue))); __p_updatingCookies = newValue } + } + private var __p_updatingCookies: (Bool)? + + public var errorMessage: String? { + get { invocations.append(.p_errorMessage_get); return __p_errorMessage ?? optionalGivenGetterValue(.p_errorMessage_get, "WebviewCookiesUpdateProtocolMock - stub value for errorMessage was not defined") } + set { invocations.append(.p_errorMessage_set(.value(newValue))); __p_errorMessage = newValue } + } + private var __p_errorMessage: (String)? + + + + + + open func updateCookies(force: Bool, retryCount: Int) { + addInvocation(.m_updateCookies__force_forceretryCount_retryCount(Parameter.value(`force`), Parameter.value(`retryCount`))) + let perform = methodPerformValue(.m_updateCookies__force_forceretryCount_retryCount(Parameter.value(`force`), Parameter.value(`retryCount`))) as? (Bool, Int) -> Void + perform?(`force`, `retryCount`) + } + + + fileprivate enum MethodType { + case m_updateCookies__force_forceretryCount_retryCount(Parameter, Parameter) + case p_authInteractor_get + case p_cookiesReady_get + case p_cookiesReady_set(Parameter) + case p_updatingCookies_get + case p_updatingCookies_set(Parameter) + case p_errorMessage_get + case p_errorMessage_set(Parameter) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_updateCookies__force_forceretryCount_retryCount(let lhsForce, let lhsRetrycount), .m_updateCookies__force_forceretryCount_retryCount(let rhsForce, let rhsRetrycount)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsForce, rhs: rhsForce, with: matcher), lhsForce, rhsForce, "force")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsRetrycount, rhs: rhsRetrycount, with: matcher), lhsRetrycount, rhsRetrycount, "retryCount")) + return Matcher.ComparisonResult(results) + case (.p_authInteractor_get,.p_authInteractor_get): return Matcher.ComparisonResult.match + case (.p_cookiesReady_get,.p_cookiesReady_get): return Matcher.ComparisonResult.match + case (.p_cookiesReady_set(let left),.p_cookiesReady_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_updatingCookies_get,.p_updatingCookies_get): return Matcher.ComparisonResult.match + case (.p_updatingCookies_set(let left),.p_updatingCookies_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_errorMessage_get,.p_errorMessage_get): return Matcher.ComparisonResult.match + case (.p_errorMessage_set(let left),.p_errorMessage_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + default: return .none + } + } + + func intValue() -> Int { + switch self { + case let .m_updateCookies__force_forceretryCount_retryCount(p0, p1): return p0.intValue + p1.intValue + case .p_authInteractor_get: return 0 + case .p_cookiesReady_get: return 0 + case .p_cookiesReady_set(let newValue): return newValue.intValue + case .p_updatingCookies_get: return 0 + case .p_updatingCookies_set(let newValue): return newValue.intValue + case .p_errorMessage_get: return 0 + case .p_errorMessage_set(let newValue): return newValue.intValue + } + } + func assertionName() -> String { + switch self { + case .m_updateCookies__force_forceretryCount_retryCount: return ".updateCookies(force:retryCount:)" + case .p_authInteractor_get: return "[get] .authInteractor" + case .p_cookiesReady_get: return "[get] .cookiesReady" + case .p_cookiesReady_set: return "[set] .cookiesReady" + case .p_updatingCookies_get: return "[get] .updatingCookies" + case .p_updatingCookies_set: return "[set] .updatingCookies" + case .p_errorMessage_get: return "[get] .errorMessage" + case .p_errorMessage_set: return "[set] .errorMessage" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + public static func authInteractor(getter defaultValue: AuthInteractorProtocol...) -> PropertyStub { + return Given(method: .p_authInteractor_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func cookiesReady(getter defaultValue: Bool...) -> PropertyStub { + return Given(method: .p_cookiesReady_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func updatingCookies(getter defaultValue: Bool...) -> PropertyStub { + return Given(method: .p_updatingCookies_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func errorMessage(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_errorMessage_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + + } + + public struct Verify { + fileprivate var method: MethodType + + public static func updateCookies(force: Parameter, retryCount: Parameter) -> Verify { return Verify(method: .m_updateCookies__force_forceretryCount_retryCount(`force`, `retryCount`))} + public static var authInteractor: Verify { return Verify(method: .p_authInteractor_get) } + public static var cookiesReady: Verify { return Verify(method: .p_cookiesReady_get) } + public static func cookiesReady(set newValue: Parameter) -> Verify { return Verify(method: .p_cookiesReady_set(newValue)) } + public static var updatingCookies: Verify { return Verify(method: .p_updatingCookies_get) } + public static func updatingCookies(set newValue: Parameter) -> Verify { return Verify(method: .p_updatingCookies_set(newValue)) } + public static var errorMessage: Verify { return Verify(method: .p_errorMessage_get) } + public static func errorMessage(set newValue: Parameter) -> Verify { return Verify(method: .p_errorMessage_set(newValue)) } + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + public static func updateCookies(force: Parameter, retryCount: Parameter, perform: @escaping (Bool, Int) -> Void) -> Perform { + return Perform(method: .m_updateCookies__force_forceretryCount_retryCount(`force`, `retryCount`), performs: perform) } } diff --git a/Core/Core/Data/Persistence/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents b/Core/Core/Data/Persistence/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents index cd49e4370..2fd252f0d 100644 --- a/Core/Core/Data/Persistence/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents +++ b/Core/Core/Data/Persistence/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents @@ -1,6 +1,7 @@ + @@ -11,6 +12,7 @@ + diff --git a/Core/Core/Data/Persistence/CorePersistenceProtocol.swift b/Core/Core/Data/Persistence/CorePersistenceProtocol.swift index 3a577ec50..3a6ec2acf 100644 --- a/Core/Core/Data/Persistence/CorePersistenceProtocol.swift +++ b/Core/Core/Data/Persistence/CorePersistenceProtocol.swift @@ -9,12 +9,14 @@ import CoreData import Combine public protocol CorePersistenceProtocol { + func set(userId: Int) + func getUserID() -> Int? func publisher() -> AnyPublisher func addToDownloadQueue(blocks: [CourseBlock], downloadQuality: DownloadQuality) - func getNextBlockForDownloading() -> DownloadDataTask? + func nextBlockForDownloading() -> DownloadDataTask? func updateDownloadState(id: String, state: DownloadState, resumeData: Data?) func deleteDownloadDataTask(id: String) throws - func saveDownloadDataTask(data: DownloadDataTask) + func saveDownloadDataTask(_ task: DownloadDataTask) func downloadDataTask(for blockId: String) -> DownloadDataTask? func downloadDataTask(for blockId: String, completion: @escaping (DownloadDataTask?) -> Void) func getDownloadDataTasks(completion: @escaping ([DownloadDataTask]) -> Void) diff --git a/Core/Core/Network/DownloadManager.swift b/Core/Core/Network/DownloadManager.swift index 4c2e6879f..2f967597a 100644 --- a/Core/Core/Network/DownloadManager.swift +++ b/Core/Core/Network/DownloadManager.swift @@ -33,6 +33,8 @@ public enum DownloadType: String { public struct DownloadDataTask: Identifiable, Hashable { public let id: String public let courseId: String + public let blockId: String + public let userId: Int public let url: String public let fileName: String public let displayName: String @@ -52,7 +54,9 @@ public struct DownloadDataTask: Identifiable, Hashable { public init( id: String, + blockId: String, courseId: String, + userId: Int, url: String, fileName: String, displayName: String, @@ -64,6 +68,8 @@ public struct DownloadDataTask: Identifiable, Hashable { ) { self.id = id self.courseId = courseId + self.blockId = blockId + self.userId = userId self.url = url self.fileName = fileName self.displayName = displayName @@ -73,6 +79,21 @@ public struct DownloadDataTask: Identifiable, Hashable { self.type = type self.fileSize = fileSize } + + public init(sourse: CDDownloadData) { + self.id = sourse.id ?? "" + self.blockId = sourse.blockId ?? "" + self.courseId = sourse.courseId ?? "" + self.userId = Int(sourse.userId) + self.url = sourse.url ?? "" + self.fileName = sourse.fileName ?? "" + self.displayName = sourse.displayName ?? "" + self.progress = sourse.progress + self.resumeData = sourse.resumeData + self.state = DownloadState(rawValue: sourse.state ?? "") ?? .waiting + self.type = DownloadType(rawValue: sourse.type ?? "") ?? .video + self.fileSize = Int(sourse.fileSize) + } } public class NoWiFiError: LocalizedError { @@ -85,19 +106,24 @@ public protocol DownloadManagerProtocol { func publisher() -> AnyPublisher func eventPublisher() -> AnyPublisher + func addToDownloadQueue(blocks: [CourseBlock]) throws + func getDownloadTasks() async -> [DownloadDataTask] func getDownloadTasksForCourse(_ courseId: String) async -> [DownloadDataTask] + func cancelDownloading(courseId: String, blocks: [CourseBlock]) async throws func cancelDownloading(task: DownloadDataTask) async throws func cancelDownloading(courseId: String) async throws + func cancelAllDownloading() async throws + func deleteFile(blocks: [CourseBlock]) async func deleteAllFiles() async + func fileUrl(for blockId: String) async -> URL? - func addToDownloadQueue(blocks: [CourseBlock]) throws - func isLargeVideosSize(blocks: [CourseBlock]) -> Bool func resumeDownloading() throws func fileUrl(for blockId: String) -> URL? + func isLargeVideosSize(blocks: [CourseBlock]) -> Bool } public enum DownloadManagerEvent { @@ -106,8 +132,9 @@ public enum DownloadManagerEvent { case progress(Double, DownloadDataTask) case paused(DownloadDataTask) case canceled(DownloadDataTask) - case finished(DownloadDataTask) case courseCanceled(String) + case allCanceled + case finished(DownloadDataTask) case deletedFile(String) case clearedAll } @@ -138,6 +165,9 @@ public class DownloadManager: DownloadManagerProtocol { connectivity: ConnectivityProtocol ) { self.persistence = persistence + if let userId = appStorage.user?.id { + self.persistence.set(userId: userId) + } self.appStorage = appStorage self.connectivity = connectivity self.backgroundTask() @@ -200,10 +230,10 @@ public class DownloadManager: DownloadManagerProtocol { public func cancelDownloading(courseId: String, blocks: [CourseBlock]) async throws { downloadRequest?.cancel() - let downloaded = await getDownloadTasksForCourse(courseId).filter { $0.state == .finished } - let blocksForDelete = blocks.filter { block in downloaded.first(where: { $0.id == block.id }) == nil } - + let blocksForDelete = blocks.filter { block in + downloaded.first(where: { $0.blockId == block.id }) == nil + } await deleteFile(blocks: blocksForDelete) downloaded.forEach { currentDownloadEventPublisher.send(.canceled($0)) @@ -226,22 +256,21 @@ public class DownloadManager: DownloadManagerProtocol { } public func cancelDownloading(courseId: String) async throws { - let downloads = await getDownloadTasksForCourse(courseId) - for downloadData in downloads { - do { - try persistence.deleteDownloadDataTask(id: downloadData.id) - if let fileUrl = await fileUrl(for: downloadData.id) { - try FileManager.default.removeItem(at: fileUrl) - } - } catch { - debugLog("Error deleting file: \(error.localizedDescription)") - } - } + let tasks = await getDownloadTasksForCourse(courseId) + await cancel(tasks: tasks) currentDownloadEventPublisher.send(.courseCanceled(courseId)) downloadRequest?.cancel() try newDownload() } + public func cancelAllDownloading() async throws { + let tasks = await getDownloadTasks().filter { $0.state != .finished } + await cancel(tasks: tasks) + currentDownloadEventPublisher.send(.allCanceled) + downloadRequest?.cancel() + try newDownload() + } + public func deleteFile(blocks: [CourseBlock]) async { for block in blocks { do { @@ -293,11 +322,13 @@ public class DownloadManager: DownloadManagerProtocol { return path?.appendingPathComponent(fileName) } + // MARK: - Private Intents + private func newDownload() throws { guard userCanDownload() else { throw NoWiFiError() } - guard let downloadTask = persistence.getNextBlockForDownloading() else { + guard let downloadTask = persistence.nextBlockForDownloading() else { isDownloadingInProgress = false return } @@ -378,7 +409,18 @@ public class DownloadManager: DownloadManagerProtocol { } } - // MARK: - Private Intents + private func cancel(tasks: [DownloadDataTask]) async { + for task in tasks { + do { + try persistence.deleteDownloadDataTask(id: task.id) + if let fileUrl = await fileUrl(for: task.id) { + try FileManager.default.removeItem(at: fileUrl) + } + } catch { + debugLog("Error deleting file: \(error.localizedDescription)") + } + } + } private func backgroundTask() { backgroundTaskProvider.eventPublisher() @@ -394,8 +436,8 @@ public class DownloadManager: DownloadManagerProtocol { lazy var videosFolderUrl: URL? = { let documentDirectoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] - let directoryURL = documentDirectoryURL.appendingPathComponent("Files", isDirectory: true) - + let directoryURL = documentDirectoryURL.appendingPathComponent(folderPathComponent, isDirectory: true) + if FileManager.default.fileExists(atPath: directoryURL.path) { return URL(fileURLWithPath: directoryURL.path) } else { @@ -413,6 +455,13 @@ public class DownloadManager: DownloadManagerProtocol { } }() + private var folderPathComponent: String { + if let id = appStorage.user?.id { + return "\(id)_Files" + } + return "Files" + } + private func saveFile(fileName: String, data: Data, folderURL: URL) { let fileURL = folderURL.appendingPathComponent(fileName) do { @@ -522,7 +571,9 @@ public class DownloadManagerMock: DownloadManagerProtocol { .canceled( .init( id: "", + blockId: "", courseId: "", + userId: 0, url: "", fileName: "", displayName: "", @@ -562,6 +613,10 @@ public class DownloadManagerMock: DownloadManagerProtocol { } + public func cancelAllDownloading() async throws { + + } + public func resumeDownloading() { } diff --git a/Course/Course/Presentation/Container/CourseContainerView.swift b/Course/Course/Presentation/Container/CourseContainerView.swift index 2e154ba69..3bd16c4af 100644 --- a/Course/Course/Presentation/Container/CourseContainerView.swift +++ b/Course/Course/Presentation/Container/CourseContainerView.swift @@ -82,7 +82,7 @@ public struct CourseContainerView: View { } .navigationBarHidden(false) .navigationBarBackButtonHidden(false) - .navigationTitle(titleBar()) + .navigationTitle(title) .onChange(of: selection, perform: didSelect) .background(Theme.Colors.background) } @@ -211,23 +211,6 @@ public struct CourseContainerView: View { ) } } - - private func titleBar() -> String { - switch CourseTab(rawValue: selection) { - case .course: - return self.title - case .videos: - return self.title - case .dates: - return CourseLocalization.CourseContainer.dates - case .discussion: - return DiscussionLocalization.title - case .handounds: - return CourseLocalization.CourseContainer.handouts - default: - return "" - } - } } #if DEBUG diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift index ce5463cd9..c26435d06 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -349,7 +349,7 @@ public class CourseContainerViewModel: BaseCourseViewModel { for vertical in sequential.childs where vertical.isDownloadable { var verticalsChilds: [DownloadViewState] = [] for block in vertical.childs where block.isDownloadable { - if let download = courseDownloadTasks.first(where: { $0.id == block.id }) { + if let download = courseDownloadTasks.first(where: { $0.blockId == block.id }) { switch download.state { case .waiting, .inProgress: sequentialsChilds.append(.downloading) diff --git a/Course/CourseTests/CourseMock.generated.swift b/Course/CourseTests/CourseMock.generated.swift index 3acd549eb..dabfab196 100644 --- a/Course/CourseTests/CourseMock.generated.swift +++ b/Course/CourseTests/CourseMock.generated.swift @@ -2123,6 +2123,19 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { return __value } + open func addToDownloadQueue(blocks: [CourseBlock]) throws { + addInvocation(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) + let perform = methodPerformValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void + perform?(`blocks`) + do { + _ = try methodReturnValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + open func getDownloadTasks() -> [DownloadDataTask] { addInvocation(.m_getDownloadTasks) let perform = methodPerformValue(.m_getDownloadTasks) as? () -> Void @@ -2190,6 +2203,19 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { } } + open func cancelAllDownloading() throws { + addInvocation(.m_cancelAllDownloading) + let perform = methodPerformValue(.m_cancelAllDownloading) as? () -> Void + perform?() + do { + _ = try methodReturnValue(.m_cancelAllDownloading).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + open func deleteFile(blocks: [CourseBlock]) { addInvocation(.m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) let perform = methodPerformValue(.m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void @@ -2215,12 +2241,12 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { return __value } - open func addToDownloadQueue(blocks: [CourseBlock]) throws { - addInvocation(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) - let perform = methodPerformValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void - perform?(`blocks`) + open func resumeDownloading() throws { + addInvocation(.m_resumeDownloading) + let perform = methodPerformValue(.m_resumeDownloading) as? () -> Void + perform?() do { - _ = try methodReturnValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))).casted() as Void + _ = try methodReturnValue(.m_resumeDownloading).casted() as Void } catch MockError.notStubed { // do nothing } catch { @@ -2242,34 +2268,22 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { return __value } - open func resumeDownloading() throws { - addInvocation(.m_resumeDownloading) - let perform = methodPerformValue(.m_resumeDownloading) as? () -> Void - perform?() - do { - _ = try methodReturnValue(.m_resumeDownloading).casted() as Void - } catch MockError.notStubed { - // do nothing - } catch { - throw error - } - } - fileprivate enum MethodType { case m_publisher case m_eventPublisher + case m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>) case m_getDownloadTasks case m_getDownloadTasksForCourse__courseId(Parameter) case m_cancelDownloading__courseId_courseIdblocks_blocks(Parameter, Parameter<[CourseBlock]>) case m_cancelDownloading__task_task(Parameter) case m_cancelDownloading__courseId_courseId(Parameter) + case m_cancelAllDownloading case m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>) case m_deleteAllFiles case m_fileUrl__for_blockId(Parameter) - case m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>) - case m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>) case m_resumeDownloading + case m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>) case p_currentDownloadTask_get static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { @@ -2278,6 +2292,11 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { case (.m_eventPublisher, .m_eventPublisher): return .match + case (.m_addToDownloadQueue__blocks_blocks(let lhsBlocks), .m_addToDownloadQueue__blocks_blocks(let rhsBlocks)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) + return Matcher.ComparisonResult(results) + case (.m_getDownloadTasks, .m_getDownloadTasks): return .match case (.m_getDownloadTasksForCourse__courseId(let lhsCourseid), .m_getDownloadTasksForCourse__courseId(let rhsCourseid)): @@ -2301,6 +2320,8 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) return Matcher.ComparisonResult(results) + case (.m_cancelAllDownloading, .m_cancelAllDownloading): return .match + case (.m_deleteFile__blocks_blocks(let lhsBlocks), .m_deleteFile__blocks_blocks(let rhsBlocks)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) @@ -2313,17 +2334,12 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockId")) return Matcher.ComparisonResult(results) - case (.m_addToDownloadQueue__blocks_blocks(let lhsBlocks), .m_addToDownloadQueue__blocks_blocks(let rhsBlocks)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) - return Matcher.ComparisonResult(results) + case (.m_resumeDownloading, .m_resumeDownloading): return .match case (.m_isLargeVideosSize__blocks_blocks(let lhsBlocks), .m_isLargeVideosSize__blocks_blocks(let rhsBlocks)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) return Matcher.ComparisonResult(results) - - case (.m_resumeDownloading, .m_resumeDownloading): return .match case (.p_currentDownloadTask_get,.p_currentDownloadTask_get): return Matcher.ComparisonResult.match default: return .none } @@ -2333,17 +2349,18 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { switch self { case .m_publisher: return 0 case .m_eventPublisher: return 0 + case let .m_addToDownloadQueue__blocks_blocks(p0): return p0.intValue case .m_getDownloadTasks: return 0 case let .m_getDownloadTasksForCourse__courseId(p0): return p0.intValue case let .m_cancelDownloading__courseId_courseIdblocks_blocks(p0, p1): return p0.intValue + p1.intValue case let .m_cancelDownloading__task_task(p0): return p0.intValue case let .m_cancelDownloading__courseId_courseId(p0): return p0.intValue + case .m_cancelAllDownloading: return 0 case let .m_deleteFile__blocks_blocks(p0): return p0.intValue case .m_deleteAllFiles: return 0 case let .m_fileUrl__for_blockId(p0): return p0.intValue - case let .m_addToDownloadQueue__blocks_blocks(p0): return p0.intValue - case let .m_isLargeVideosSize__blocks_blocks(p0): return p0.intValue case .m_resumeDownloading: return 0 + case let .m_isLargeVideosSize__blocks_blocks(p0): return p0.intValue case .p_currentDownloadTask_get: return 0 } } @@ -2351,17 +2368,18 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { switch self { case .m_publisher: return ".publisher()" case .m_eventPublisher: return ".eventPublisher()" + case .m_addToDownloadQueue__blocks_blocks: return ".addToDownloadQueue(blocks:)" case .m_getDownloadTasks: return ".getDownloadTasks()" case .m_getDownloadTasksForCourse__courseId: return ".getDownloadTasksForCourse(_:)" case .m_cancelDownloading__courseId_courseIdblocks_blocks: return ".cancelDownloading(courseId:blocks:)" case .m_cancelDownloading__task_task: return ".cancelDownloading(task:)" case .m_cancelDownloading__courseId_courseId: return ".cancelDownloading(courseId:)" + case .m_cancelAllDownloading: return ".cancelAllDownloading()" case .m_deleteFile__blocks_blocks: return ".deleteFile(blocks:)" case .m_deleteAllFiles: return ".deleteAllFiles()" case .m_fileUrl__for_blockId: return ".fileUrl(for:)" - case .m_addToDownloadQueue__blocks_blocks: return ".addToDownloadQueue(blocks:)" - case .m_isLargeVideosSize__blocks_blocks: return ".isLargeVideosSize(blocks:)" case .m_resumeDownloading: return ".resumeDownloading()" + case .m_isLargeVideosSize__blocks_blocks: return ".isLargeVideosSize(blocks:)" case .p_currentDownloadTask_get: return "[get] .currentDownloadTask" } } @@ -2439,6 +2457,16 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { willProduce(stubber) return given } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willThrow: Error...) -> MethodStub { + return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } public static func cancelDownloading(courseId: Parameter, blocks: Parameter<[CourseBlock]>, willThrow: Error...) -> MethodStub { return Given(method: .m_cancelDownloading__courseId_courseIdblocks_blocks(`courseId`, `blocks`), products: willThrow.map({ StubProduct.throw($0) })) } @@ -2469,12 +2497,12 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { willProduce(stubber) return given } - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willThrow: Error...) -> MethodStub { - return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) + public static func cancelAllDownloading(willThrow: Error...) -> MethodStub { + return Given(method: .m_cancelAllDownloading, products: willThrow.map({ StubProduct.throw($0) })) } - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willProduce: (StubberThrows) -> Void) -> MethodStub { + public static func cancelAllDownloading(willProduce: (StubberThrows) -> Void) -> MethodStub { let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) }() + let given: Given = { return Given(method: .m_cancelAllDownloading, products: willThrow.map({ StubProduct.throw($0) })) }() let stubber = given.stubThrows(for: (Void).self) willProduce(stubber) return given @@ -2496,17 +2524,18 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func publisher() -> Verify { return Verify(method: .m_publisher)} public static func eventPublisher() -> Verify { return Verify(method: .m_eventPublisher)} + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_addToDownloadQueue__blocks_blocks(`blocks`))} public static func getDownloadTasks() -> Verify { return Verify(method: .m_getDownloadTasks)} public static func getDownloadTasksForCourse(_ courseId: Parameter) -> Verify { return Verify(method: .m_getDownloadTasksForCourse__courseId(`courseId`))} public static func cancelDownloading(courseId: Parameter, blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_cancelDownloading__courseId_courseIdblocks_blocks(`courseId`, `blocks`))} public static func cancelDownloading(task: Parameter) -> Verify { return Verify(method: .m_cancelDownloading__task_task(`task`))} public static func cancelDownloading(courseId: Parameter) -> Verify { return Verify(method: .m_cancelDownloading__courseId_courseId(`courseId`))} + public static func cancelAllDownloading() -> Verify { return Verify(method: .m_cancelAllDownloading)} public static func deleteFile(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_deleteFile__blocks_blocks(`blocks`))} public static func deleteAllFiles() -> Verify { return Verify(method: .m_deleteAllFiles)} public static func fileUrl(for blockId: Parameter) -> Verify { return Verify(method: .m_fileUrl__for_blockId(`blockId`))} - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_addToDownloadQueue__blocks_blocks(`blocks`))} - public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_isLargeVideosSize__blocks_blocks(`blocks`))} public static func resumeDownloading() -> Verify { return Verify(method: .m_resumeDownloading)} + public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_isLargeVideosSize__blocks_blocks(`blocks`))} public static var currentDownloadTask: Verify { return Verify(method: .p_currentDownloadTask_get) } } @@ -2520,6 +2549,9 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func eventPublisher(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_eventPublisher, performs: perform) } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { + return Perform(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), performs: perform) + } public static func getDownloadTasks(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_getDownloadTasks, performs: perform) } @@ -2535,6 +2567,9 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func cancelDownloading(courseId: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_cancelDownloading__courseId_courseId(`courseId`), performs: perform) } + public static func cancelAllDownloading(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_cancelAllDownloading, performs: perform) + } public static func deleteFile(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { return Perform(method: .m_deleteFile__blocks_blocks(`blocks`), performs: perform) } @@ -2544,14 +2579,257 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func fileUrl(for blockId: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_fileUrl__for_blockId(`blockId`), performs: perform) } - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { - return Perform(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), performs: perform) + public static func resumeDownloading(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_resumeDownloading, performs: perform) } public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { return Perform(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), performs: perform) } - public static func resumeDownloading(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_resumeDownloading, performs: perform) + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - WebviewCookiesUpdateProtocol + +open class WebviewCookiesUpdateProtocolMock: WebviewCookiesUpdateProtocol, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + public var authInteractor: AuthInteractorProtocol { + get { invocations.append(.p_authInteractor_get); return __p_authInteractor ?? givenGetterValue(.p_authInteractor_get, "WebviewCookiesUpdateProtocolMock - stub value for authInteractor was not defined") } + } + private var __p_authInteractor: (AuthInteractorProtocol)? + + public var cookiesReady: Bool { + get { invocations.append(.p_cookiesReady_get); return __p_cookiesReady ?? givenGetterValue(.p_cookiesReady_get, "WebviewCookiesUpdateProtocolMock - stub value for cookiesReady was not defined") } + set { invocations.append(.p_cookiesReady_set(.value(newValue))); __p_cookiesReady = newValue } + } + private var __p_cookiesReady: (Bool)? + + public var updatingCookies: Bool { + get { invocations.append(.p_updatingCookies_get); return __p_updatingCookies ?? givenGetterValue(.p_updatingCookies_get, "WebviewCookiesUpdateProtocolMock - stub value for updatingCookies was not defined") } + set { invocations.append(.p_updatingCookies_set(.value(newValue))); __p_updatingCookies = newValue } + } + private var __p_updatingCookies: (Bool)? + + public var errorMessage: String? { + get { invocations.append(.p_errorMessage_get); return __p_errorMessage ?? optionalGivenGetterValue(.p_errorMessage_get, "WebviewCookiesUpdateProtocolMock - stub value for errorMessage was not defined") } + set { invocations.append(.p_errorMessage_set(.value(newValue))); __p_errorMessage = newValue } + } + private var __p_errorMessage: (String)? + + + + + + open func updateCookies(force: Bool, retryCount: Int) { + addInvocation(.m_updateCookies__force_forceretryCount_retryCount(Parameter.value(`force`), Parameter.value(`retryCount`))) + let perform = methodPerformValue(.m_updateCookies__force_forceretryCount_retryCount(Parameter.value(`force`), Parameter.value(`retryCount`))) as? (Bool, Int) -> Void + perform?(`force`, `retryCount`) + } + + + fileprivate enum MethodType { + case m_updateCookies__force_forceretryCount_retryCount(Parameter, Parameter) + case p_authInteractor_get + case p_cookiesReady_get + case p_cookiesReady_set(Parameter) + case p_updatingCookies_get + case p_updatingCookies_set(Parameter) + case p_errorMessage_get + case p_errorMessage_set(Parameter) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_updateCookies__force_forceretryCount_retryCount(let lhsForce, let lhsRetrycount), .m_updateCookies__force_forceretryCount_retryCount(let rhsForce, let rhsRetrycount)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsForce, rhs: rhsForce, with: matcher), lhsForce, rhsForce, "force")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsRetrycount, rhs: rhsRetrycount, with: matcher), lhsRetrycount, rhsRetrycount, "retryCount")) + return Matcher.ComparisonResult(results) + case (.p_authInteractor_get,.p_authInteractor_get): return Matcher.ComparisonResult.match + case (.p_cookiesReady_get,.p_cookiesReady_get): return Matcher.ComparisonResult.match + case (.p_cookiesReady_set(let left),.p_cookiesReady_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_updatingCookies_get,.p_updatingCookies_get): return Matcher.ComparisonResult.match + case (.p_updatingCookies_set(let left),.p_updatingCookies_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_errorMessage_get,.p_errorMessage_get): return Matcher.ComparisonResult.match + case (.p_errorMessage_set(let left),.p_errorMessage_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + default: return .none + } + } + + func intValue() -> Int { + switch self { + case let .m_updateCookies__force_forceretryCount_retryCount(p0, p1): return p0.intValue + p1.intValue + case .p_authInteractor_get: return 0 + case .p_cookiesReady_get: return 0 + case .p_cookiesReady_set(let newValue): return newValue.intValue + case .p_updatingCookies_get: return 0 + case .p_updatingCookies_set(let newValue): return newValue.intValue + case .p_errorMessage_get: return 0 + case .p_errorMessage_set(let newValue): return newValue.intValue + } + } + func assertionName() -> String { + switch self { + case .m_updateCookies__force_forceretryCount_retryCount: return ".updateCookies(force:retryCount:)" + case .p_authInteractor_get: return "[get] .authInteractor" + case .p_cookiesReady_get: return "[get] .cookiesReady" + case .p_cookiesReady_set: return "[set] .cookiesReady" + case .p_updatingCookies_get: return "[get] .updatingCookies" + case .p_updatingCookies_set: return "[set] .updatingCookies" + case .p_errorMessage_get: return "[get] .errorMessage" + case .p_errorMessage_set: return "[set] .errorMessage" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + public static func authInteractor(getter defaultValue: AuthInteractorProtocol...) -> PropertyStub { + return Given(method: .p_authInteractor_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func cookiesReady(getter defaultValue: Bool...) -> PropertyStub { + return Given(method: .p_cookiesReady_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func updatingCookies(getter defaultValue: Bool...) -> PropertyStub { + return Given(method: .p_updatingCookies_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func errorMessage(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_errorMessage_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + + } + + public struct Verify { + fileprivate var method: MethodType + + public static func updateCookies(force: Parameter, retryCount: Parameter) -> Verify { return Verify(method: .m_updateCookies__force_forceretryCount_retryCount(`force`, `retryCount`))} + public static var authInteractor: Verify { return Verify(method: .p_authInteractor_get) } + public static var cookiesReady: Verify { return Verify(method: .p_cookiesReady_get) } + public static func cookiesReady(set newValue: Parameter) -> Verify { return Verify(method: .p_cookiesReady_set(newValue)) } + public static var updatingCookies: Verify { return Verify(method: .p_updatingCookies_get) } + public static func updatingCookies(set newValue: Parameter) -> Verify { return Verify(method: .p_updatingCookies_set(newValue)) } + public static var errorMessage: Verify { return Verify(method: .p_errorMessage_get) } + public static func errorMessage(set newValue: Parameter) -> Verify { return Verify(method: .p_errorMessage_set(newValue)) } + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + public static func updateCookies(force: Parameter, retryCount: Parameter, perform: @escaping (Bool, Int) -> Void) -> Perform { + return Perform(method: .m_updateCookies__force_forceretryCount_retryCount(`force`, `retryCount`), performs: perform) } } diff --git a/Course/CourseTests/Presentation/Container/CourseContainerViewModelTests.swift b/Course/CourseTests/Presentation/Container/CourseContainerViewModelTests.swift index a62719c81..b43f88a9e 100644 --- a/Course/CourseTests/Presentation/Container/CourseContainerViewModelTests.swift +++ b/Course/CourseTests/Presentation/Container/CourseContainerViewModelTests.swift @@ -412,7 +412,9 @@ final class CourseContainerViewModelTests: XCTestCase { let downloadData = DownloadDataTask( id: "1", + blockId: "1", courseId: "course123", + userId: 1, url: "https://example.com/file.mp4", fileName: "file.mp4", displayName: "file.mp4", @@ -884,7 +886,9 @@ final class CourseContainerViewModelTests: XCTestCase { let downloadData = DownloadDataTask( id: "1", + blockId: "1", courseId: "course123", + userId: 1, url: "https://example.com/file.mp4", fileName: "file.mp4", displayName: "file.mp4", @@ -1007,7 +1011,9 @@ final class CourseContainerViewModelTests: XCTestCase { let downloadData = DownloadDataTask( id: "1", + blockId: "1", courseId: "course123", + userId: 1, url: "https://example.com/file.mp4", fileName: "file.mp4", displayName: "file.mp4", @@ -1148,7 +1154,9 @@ final class CourseContainerViewModelTests: XCTestCase { let downloadData = DownloadDataTask( id: "1", + blockId: "1", courseId: "course123", + userId: 1, url: "https://example.com/file.mp4", fileName: "file.mp4", displayName: "file.mp4", diff --git a/Dashboard/DashboardTests/DashboardMock.generated.swift b/Dashboard/DashboardTests/DashboardMock.generated.swift index 2aa0f593d..b8a417000 100644 --- a/Dashboard/DashboardTests/DashboardMock.generated.swift +++ b/Dashboard/DashboardTests/DashboardMock.generated.swift @@ -1601,6 +1601,19 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { return __value } + open func addToDownloadQueue(blocks: [CourseBlock]) throws { + addInvocation(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) + let perform = methodPerformValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void + perform?(`blocks`) + do { + _ = try methodReturnValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + open func getDownloadTasks() -> [DownloadDataTask] { addInvocation(.m_getDownloadTasks) let perform = methodPerformValue(.m_getDownloadTasks) as? () -> Void @@ -1668,6 +1681,19 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { } } + open func cancelAllDownloading() throws { + addInvocation(.m_cancelAllDownloading) + let perform = methodPerformValue(.m_cancelAllDownloading) as? () -> Void + perform?() + do { + _ = try methodReturnValue(.m_cancelAllDownloading).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + open func deleteFile(blocks: [CourseBlock]) { addInvocation(.m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) let perform = methodPerformValue(.m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void @@ -1693,12 +1719,12 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { return __value } - open func addToDownloadQueue(blocks: [CourseBlock]) throws { - addInvocation(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) - let perform = methodPerformValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void - perform?(`blocks`) + open func resumeDownloading() throws { + addInvocation(.m_resumeDownloading) + let perform = methodPerformValue(.m_resumeDownloading) as? () -> Void + perform?() do { - _ = try methodReturnValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))).casted() as Void + _ = try methodReturnValue(.m_resumeDownloading).casted() as Void } catch MockError.notStubed { // do nothing } catch { @@ -1720,34 +1746,22 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { return __value } - open func resumeDownloading() throws { - addInvocation(.m_resumeDownloading) - let perform = methodPerformValue(.m_resumeDownloading) as? () -> Void - perform?() - do { - _ = try methodReturnValue(.m_resumeDownloading).casted() as Void - } catch MockError.notStubed { - // do nothing - } catch { - throw error - } - } - fileprivate enum MethodType { case m_publisher case m_eventPublisher + case m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>) case m_getDownloadTasks case m_getDownloadTasksForCourse__courseId(Parameter) case m_cancelDownloading__courseId_courseIdblocks_blocks(Parameter, Parameter<[CourseBlock]>) case m_cancelDownloading__task_task(Parameter) case m_cancelDownloading__courseId_courseId(Parameter) + case m_cancelAllDownloading case m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>) case m_deleteAllFiles case m_fileUrl__for_blockId(Parameter) - case m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>) - case m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>) case m_resumeDownloading + case m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>) case p_currentDownloadTask_get static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { @@ -1756,6 +1770,11 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { case (.m_eventPublisher, .m_eventPublisher): return .match + case (.m_addToDownloadQueue__blocks_blocks(let lhsBlocks), .m_addToDownloadQueue__blocks_blocks(let rhsBlocks)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) + return Matcher.ComparisonResult(results) + case (.m_getDownloadTasks, .m_getDownloadTasks): return .match case (.m_getDownloadTasksForCourse__courseId(let lhsCourseid), .m_getDownloadTasksForCourse__courseId(let rhsCourseid)): @@ -1779,6 +1798,8 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) return Matcher.ComparisonResult(results) + case (.m_cancelAllDownloading, .m_cancelAllDownloading): return .match + case (.m_deleteFile__blocks_blocks(let lhsBlocks), .m_deleteFile__blocks_blocks(let rhsBlocks)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) @@ -1791,17 +1812,12 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockId")) return Matcher.ComparisonResult(results) - case (.m_addToDownloadQueue__blocks_blocks(let lhsBlocks), .m_addToDownloadQueue__blocks_blocks(let rhsBlocks)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) - return Matcher.ComparisonResult(results) + case (.m_resumeDownloading, .m_resumeDownloading): return .match case (.m_isLargeVideosSize__blocks_blocks(let lhsBlocks), .m_isLargeVideosSize__blocks_blocks(let rhsBlocks)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) return Matcher.ComparisonResult(results) - - case (.m_resumeDownloading, .m_resumeDownloading): return .match case (.p_currentDownloadTask_get,.p_currentDownloadTask_get): return Matcher.ComparisonResult.match default: return .none } @@ -1811,17 +1827,18 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { switch self { case .m_publisher: return 0 case .m_eventPublisher: return 0 + case let .m_addToDownloadQueue__blocks_blocks(p0): return p0.intValue case .m_getDownloadTasks: return 0 case let .m_getDownloadTasksForCourse__courseId(p0): return p0.intValue case let .m_cancelDownloading__courseId_courseIdblocks_blocks(p0, p1): return p0.intValue + p1.intValue case let .m_cancelDownloading__task_task(p0): return p0.intValue case let .m_cancelDownloading__courseId_courseId(p0): return p0.intValue + case .m_cancelAllDownloading: return 0 case let .m_deleteFile__blocks_blocks(p0): return p0.intValue case .m_deleteAllFiles: return 0 case let .m_fileUrl__for_blockId(p0): return p0.intValue - case let .m_addToDownloadQueue__blocks_blocks(p0): return p0.intValue - case let .m_isLargeVideosSize__blocks_blocks(p0): return p0.intValue case .m_resumeDownloading: return 0 + case let .m_isLargeVideosSize__blocks_blocks(p0): return p0.intValue case .p_currentDownloadTask_get: return 0 } } @@ -1829,17 +1846,18 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { switch self { case .m_publisher: return ".publisher()" case .m_eventPublisher: return ".eventPublisher()" + case .m_addToDownloadQueue__blocks_blocks: return ".addToDownloadQueue(blocks:)" case .m_getDownloadTasks: return ".getDownloadTasks()" case .m_getDownloadTasksForCourse__courseId: return ".getDownloadTasksForCourse(_:)" case .m_cancelDownloading__courseId_courseIdblocks_blocks: return ".cancelDownloading(courseId:blocks:)" case .m_cancelDownloading__task_task: return ".cancelDownloading(task:)" case .m_cancelDownloading__courseId_courseId: return ".cancelDownloading(courseId:)" + case .m_cancelAllDownloading: return ".cancelAllDownloading()" case .m_deleteFile__blocks_blocks: return ".deleteFile(blocks:)" case .m_deleteAllFiles: return ".deleteAllFiles()" case .m_fileUrl__for_blockId: return ".fileUrl(for:)" - case .m_addToDownloadQueue__blocks_blocks: return ".addToDownloadQueue(blocks:)" - case .m_isLargeVideosSize__blocks_blocks: return ".isLargeVideosSize(blocks:)" case .m_resumeDownloading: return ".resumeDownloading()" + case .m_isLargeVideosSize__blocks_blocks: return ".isLargeVideosSize(blocks:)" case .p_currentDownloadTask_get: return "[get] .currentDownloadTask" } } @@ -1917,6 +1935,16 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { willProduce(stubber) return given } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willThrow: Error...) -> MethodStub { + return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } public static func cancelDownloading(courseId: Parameter, blocks: Parameter<[CourseBlock]>, willThrow: Error...) -> MethodStub { return Given(method: .m_cancelDownloading__courseId_courseIdblocks_blocks(`courseId`, `blocks`), products: willThrow.map({ StubProduct.throw($0) })) } @@ -1947,12 +1975,12 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { willProduce(stubber) return given } - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willThrow: Error...) -> MethodStub { - return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) + public static func cancelAllDownloading(willThrow: Error...) -> MethodStub { + return Given(method: .m_cancelAllDownloading, products: willThrow.map({ StubProduct.throw($0) })) } - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willProduce: (StubberThrows) -> Void) -> MethodStub { + public static func cancelAllDownloading(willProduce: (StubberThrows) -> Void) -> MethodStub { let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) }() + let given: Given = { return Given(method: .m_cancelAllDownloading, products: willThrow.map({ StubProduct.throw($0) })) }() let stubber = given.stubThrows(for: (Void).self) willProduce(stubber) return given @@ -1974,17 +2002,18 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func publisher() -> Verify { return Verify(method: .m_publisher)} public static func eventPublisher() -> Verify { return Verify(method: .m_eventPublisher)} + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_addToDownloadQueue__blocks_blocks(`blocks`))} public static func getDownloadTasks() -> Verify { return Verify(method: .m_getDownloadTasks)} public static func getDownloadTasksForCourse(_ courseId: Parameter) -> Verify { return Verify(method: .m_getDownloadTasksForCourse__courseId(`courseId`))} public static func cancelDownloading(courseId: Parameter, blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_cancelDownloading__courseId_courseIdblocks_blocks(`courseId`, `blocks`))} public static func cancelDownloading(task: Parameter) -> Verify { return Verify(method: .m_cancelDownloading__task_task(`task`))} public static func cancelDownloading(courseId: Parameter) -> Verify { return Verify(method: .m_cancelDownloading__courseId_courseId(`courseId`))} + public static func cancelAllDownloading() -> Verify { return Verify(method: .m_cancelAllDownloading)} public static func deleteFile(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_deleteFile__blocks_blocks(`blocks`))} public static func deleteAllFiles() -> Verify { return Verify(method: .m_deleteAllFiles)} public static func fileUrl(for blockId: Parameter) -> Verify { return Verify(method: .m_fileUrl__for_blockId(`blockId`))} - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_addToDownloadQueue__blocks_blocks(`blocks`))} - public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_isLargeVideosSize__blocks_blocks(`blocks`))} public static func resumeDownloading() -> Verify { return Verify(method: .m_resumeDownloading)} + public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_isLargeVideosSize__blocks_blocks(`blocks`))} public static var currentDownloadTask: Verify { return Verify(method: .p_currentDownloadTask_get) } } @@ -1998,6 +2027,9 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func eventPublisher(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_eventPublisher, performs: perform) } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { + return Perform(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), performs: perform) + } public static func getDownloadTasks(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_getDownloadTasks, performs: perform) } @@ -2013,6 +2045,9 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func cancelDownloading(courseId: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_cancelDownloading__courseId_courseId(`courseId`), performs: perform) } + public static func cancelAllDownloading(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_cancelAllDownloading, performs: perform) + } public static func deleteFile(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { return Perform(method: .m_deleteFile__blocks_blocks(`blocks`), performs: perform) } @@ -2022,14 +2057,257 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func fileUrl(for blockId: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_fileUrl__for_blockId(`blockId`), performs: perform) } - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { - return Perform(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), performs: perform) + public static func resumeDownloading(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_resumeDownloading, performs: perform) } public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { return Perform(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), performs: perform) } - public static func resumeDownloading(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_resumeDownloading, performs: perform) + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - WebviewCookiesUpdateProtocol + +open class WebviewCookiesUpdateProtocolMock: WebviewCookiesUpdateProtocol, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + public var authInteractor: AuthInteractorProtocol { + get { invocations.append(.p_authInteractor_get); return __p_authInteractor ?? givenGetterValue(.p_authInteractor_get, "WebviewCookiesUpdateProtocolMock - stub value for authInteractor was not defined") } + } + private var __p_authInteractor: (AuthInteractorProtocol)? + + public var cookiesReady: Bool { + get { invocations.append(.p_cookiesReady_get); return __p_cookiesReady ?? givenGetterValue(.p_cookiesReady_get, "WebviewCookiesUpdateProtocolMock - stub value for cookiesReady was not defined") } + set { invocations.append(.p_cookiesReady_set(.value(newValue))); __p_cookiesReady = newValue } + } + private var __p_cookiesReady: (Bool)? + + public var updatingCookies: Bool { + get { invocations.append(.p_updatingCookies_get); return __p_updatingCookies ?? givenGetterValue(.p_updatingCookies_get, "WebviewCookiesUpdateProtocolMock - stub value for updatingCookies was not defined") } + set { invocations.append(.p_updatingCookies_set(.value(newValue))); __p_updatingCookies = newValue } + } + private var __p_updatingCookies: (Bool)? + + public var errorMessage: String? { + get { invocations.append(.p_errorMessage_get); return __p_errorMessage ?? optionalGivenGetterValue(.p_errorMessage_get, "WebviewCookiesUpdateProtocolMock - stub value for errorMessage was not defined") } + set { invocations.append(.p_errorMessage_set(.value(newValue))); __p_errorMessage = newValue } + } + private var __p_errorMessage: (String)? + + + + + + open func updateCookies(force: Bool, retryCount: Int) { + addInvocation(.m_updateCookies__force_forceretryCount_retryCount(Parameter.value(`force`), Parameter.value(`retryCount`))) + let perform = methodPerformValue(.m_updateCookies__force_forceretryCount_retryCount(Parameter.value(`force`), Parameter.value(`retryCount`))) as? (Bool, Int) -> Void + perform?(`force`, `retryCount`) + } + + + fileprivate enum MethodType { + case m_updateCookies__force_forceretryCount_retryCount(Parameter, Parameter) + case p_authInteractor_get + case p_cookiesReady_get + case p_cookiesReady_set(Parameter) + case p_updatingCookies_get + case p_updatingCookies_set(Parameter) + case p_errorMessage_get + case p_errorMessage_set(Parameter) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_updateCookies__force_forceretryCount_retryCount(let lhsForce, let lhsRetrycount), .m_updateCookies__force_forceretryCount_retryCount(let rhsForce, let rhsRetrycount)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsForce, rhs: rhsForce, with: matcher), lhsForce, rhsForce, "force")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsRetrycount, rhs: rhsRetrycount, with: matcher), lhsRetrycount, rhsRetrycount, "retryCount")) + return Matcher.ComparisonResult(results) + case (.p_authInteractor_get,.p_authInteractor_get): return Matcher.ComparisonResult.match + case (.p_cookiesReady_get,.p_cookiesReady_get): return Matcher.ComparisonResult.match + case (.p_cookiesReady_set(let left),.p_cookiesReady_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_updatingCookies_get,.p_updatingCookies_get): return Matcher.ComparisonResult.match + case (.p_updatingCookies_set(let left),.p_updatingCookies_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_errorMessage_get,.p_errorMessage_get): return Matcher.ComparisonResult.match + case (.p_errorMessage_set(let left),.p_errorMessage_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + default: return .none + } + } + + func intValue() -> Int { + switch self { + case let .m_updateCookies__force_forceretryCount_retryCount(p0, p1): return p0.intValue + p1.intValue + case .p_authInteractor_get: return 0 + case .p_cookiesReady_get: return 0 + case .p_cookiesReady_set(let newValue): return newValue.intValue + case .p_updatingCookies_get: return 0 + case .p_updatingCookies_set(let newValue): return newValue.intValue + case .p_errorMessage_get: return 0 + case .p_errorMessage_set(let newValue): return newValue.intValue + } + } + func assertionName() -> String { + switch self { + case .m_updateCookies__force_forceretryCount_retryCount: return ".updateCookies(force:retryCount:)" + case .p_authInteractor_get: return "[get] .authInteractor" + case .p_cookiesReady_get: return "[get] .cookiesReady" + case .p_cookiesReady_set: return "[set] .cookiesReady" + case .p_updatingCookies_get: return "[get] .updatingCookies" + case .p_updatingCookies_set: return "[set] .updatingCookies" + case .p_errorMessage_get: return "[get] .errorMessage" + case .p_errorMessage_set: return "[set] .errorMessage" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + public static func authInteractor(getter defaultValue: AuthInteractorProtocol...) -> PropertyStub { + return Given(method: .p_authInteractor_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func cookiesReady(getter defaultValue: Bool...) -> PropertyStub { + return Given(method: .p_cookiesReady_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func updatingCookies(getter defaultValue: Bool...) -> PropertyStub { + return Given(method: .p_updatingCookies_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func errorMessage(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_errorMessage_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + + } + + public struct Verify { + fileprivate var method: MethodType + + public static func updateCookies(force: Parameter, retryCount: Parameter) -> Verify { return Verify(method: .m_updateCookies__force_forceretryCount_retryCount(`force`, `retryCount`))} + public static var authInteractor: Verify { return Verify(method: .p_authInteractor_get) } + public static var cookiesReady: Verify { return Verify(method: .p_cookiesReady_get) } + public static func cookiesReady(set newValue: Parameter) -> Verify { return Verify(method: .p_cookiesReady_set(newValue)) } + public static var updatingCookies: Verify { return Verify(method: .p_updatingCookies_get) } + public static func updatingCookies(set newValue: Parameter) -> Verify { return Verify(method: .p_updatingCookies_set(newValue)) } + public static var errorMessage: Verify { return Verify(method: .p_errorMessage_get) } + public static func errorMessage(set newValue: Parameter) -> Verify { return Verify(method: .p_errorMessage_set(newValue)) } + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + public static func updateCookies(force: Parameter, retryCount: Parameter, perform: @escaping (Bool, Int) -> Void) -> Perform { + return Perform(method: .m_updateCookies__force_forceretryCount_retryCount(`force`, `retryCount`), performs: perform) } } diff --git a/Discovery/DiscoveryTests/DiscoveryMock.generated.swift b/Discovery/DiscoveryTests/DiscoveryMock.generated.swift index 3522e8691..ef22e064f 100644 --- a/Discovery/DiscoveryTests/DiscoveryMock.generated.swift +++ b/Discovery/DiscoveryTests/DiscoveryMock.generated.swift @@ -1858,6 +1858,19 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { return __value } + open func addToDownloadQueue(blocks: [CourseBlock]) throws { + addInvocation(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) + let perform = methodPerformValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void + perform?(`blocks`) + do { + _ = try methodReturnValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + open func getDownloadTasks() -> [DownloadDataTask] { addInvocation(.m_getDownloadTasks) let perform = methodPerformValue(.m_getDownloadTasks) as? () -> Void @@ -1925,6 +1938,19 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { } } + open func cancelAllDownloading() throws { + addInvocation(.m_cancelAllDownloading) + let perform = methodPerformValue(.m_cancelAllDownloading) as? () -> Void + perform?() + do { + _ = try methodReturnValue(.m_cancelAllDownloading).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + open func deleteFile(blocks: [CourseBlock]) { addInvocation(.m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) let perform = methodPerformValue(.m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void @@ -1950,12 +1976,12 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { return __value } - open func addToDownloadQueue(blocks: [CourseBlock]) throws { - addInvocation(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) - let perform = methodPerformValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void - perform?(`blocks`) + open func resumeDownloading() throws { + addInvocation(.m_resumeDownloading) + let perform = methodPerformValue(.m_resumeDownloading) as? () -> Void + perform?() do { - _ = try methodReturnValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))).casted() as Void + _ = try methodReturnValue(.m_resumeDownloading).casted() as Void } catch MockError.notStubed { // do nothing } catch { @@ -1977,34 +2003,22 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { return __value } - open func resumeDownloading() throws { - addInvocation(.m_resumeDownloading) - let perform = methodPerformValue(.m_resumeDownloading) as? () -> Void - perform?() - do { - _ = try methodReturnValue(.m_resumeDownloading).casted() as Void - } catch MockError.notStubed { - // do nothing - } catch { - throw error - } - } - fileprivate enum MethodType { case m_publisher case m_eventPublisher + case m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>) case m_getDownloadTasks case m_getDownloadTasksForCourse__courseId(Parameter) case m_cancelDownloading__courseId_courseIdblocks_blocks(Parameter, Parameter<[CourseBlock]>) case m_cancelDownloading__task_task(Parameter) case m_cancelDownloading__courseId_courseId(Parameter) + case m_cancelAllDownloading case m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>) case m_deleteAllFiles case m_fileUrl__for_blockId(Parameter) - case m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>) - case m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>) case m_resumeDownloading + case m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>) case p_currentDownloadTask_get static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { @@ -2013,6 +2027,11 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { case (.m_eventPublisher, .m_eventPublisher): return .match + case (.m_addToDownloadQueue__blocks_blocks(let lhsBlocks), .m_addToDownloadQueue__blocks_blocks(let rhsBlocks)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) + return Matcher.ComparisonResult(results) + case (.m_getDownloadTasks, .m_getDownloadTasks): return .match case (.m_getDownloadTasksForCourse__courseId(let lhsCourseid), .m_getDownloadTasksForCourse__courseId(let rhsCourseid)): @@ -2036,6 +2055,8 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) return Matcher.ComparisonResult(results) + case (.m_cancelAllDownloading, .m_cancelAllDownloading): return .match + case (.m_deleteFile__blocks_blocks(let lhsBlocks), .m_deleteFile__blocks_blocks(let rhsBlocks)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) @@ -2048,17 +2069,12 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockId")) return Matcher.ComparisonResult(results) - case (.m_addToDownloadQueue__blocks_blocks(let lhsBlocks), .m_addToDownloadQueue__blocks_blocks(let rhsBlocks)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) - return Matcher.ComparisonResult(results) + case (.m_resumeDownloading, .m_resumeDownloading): return .match case (.m_isLargeVideosSize__blocks_blocks(let lhsBlocks), .m_isLargeVideosSize__blocks_blocks(let rhsBlocks)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) return Matcher.ComparisonResult(results) - - case (.m_resumeDownloading, .m_resumeDownloading): return .match case (.p_currentDownloadTask_get,.p_currentDownloadTask_get): return Matcher.ComparisonResult.match default: return .none } @@ -2068,17 +2084,18 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { switch self { case .m_publisher: return 0 case .m_eventPublisher: return 0 + case let .m_addToDownloadQueue__blocks_blocks(p0): return p0.intValue case .m_getDownloadTasks: return 0 case let .m_getDownloadTasksForCourse__courseId(p0): return p0.intValue case let .m_cancelDownloading__courseId_courseIdblocks_blocks(p0, p1): return p0.intValue + p1.intValue case let .m_cancelDownloading__task_task(p0): return p0.intValue case let .m_cancelDownloading__courseId_courseId(p0): return p0.intValue + case .m_cancelAllDownloading: return 0 case let .m_deleteFile__blocks_blocks(p0): return p0.intValue case .m_deleteAllFiles: return 0 case let .m_fileUrl__for_blockId(p0): return p0.intValue - case let .m_addToDownloadQueue__blocks_blocks(p0): return p0.intValue - case let .m_isLargeVideosSize__blocks_blocks(p0): return p0.intValue case .m_resumeDownloading: return 0 + case let .m_isLargeVideosSize__blocks_blocks(p0): return p0.intValue case .p_currentDownloadTask_get: return 0 } } @@ -2086,17 +2103,18 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { switch self { case .m_publisher: return ".publisher()" case .m_eventPublisher: return ".eventPublisher()" + case .m_addToDownloadQueue__blocks_blocks: return ".addToDownloadQueue(blocks:)" case .m_getDownloadTasks: return ".getDownloadTasks()" case .m_getDownloadTasksForCourse__courseId: return ".getDownloadTasksForCourse(_:)" case .m_cancelDownloading__courseId_courseIdblocks_blocks: return ".cancelDownloading(courseId:blocks:)" case .m_cancelDownloading__task_task: return ".cancelDownloading(task:)" case .m_cancelDownloading__courseId_courseId: return ".cancelDownloading(courseId:)" + case .m_cancelAllDownloading: return ".cancelAllDownloading()" case .m_deleteFile__blocks_blocks: return ".deleteFile(blocks:)" case .m_deleteAllFiles: return ".deleteAllFiles()" case .m_fileUrl__for_blockId: return ".fileUrl(for:)" - case .m_addToDownloadQueue__blocks_blocks: return ".addToDownloadQueue(blocks:)" - case .m_isLargeVideosSize__blocks_blocks: return ".isLargeVideosSize(blocks:)" case .m_resumeDownloading: return ".resumeDownloading()" + case .m_isLargeVideosSize__blocks_blocks: return ".isLargeVideosSize(blocks:)" case .p_currentDownloadTask_get: return "[get] .currentDownloadTask" } } @@ -2174,6 +2192,16 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { willProduce(stubber) return given } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willThrow: Error...) -> MethodStub { + return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } public static func cancelDownloading(courseId: Parameter, blocks: Parameter<[CourseBlock]>, willThrow: Error...) -> MethodStub { return Given(method: .m_cancelDownloading__courseId_courseIdblocks_blocks(`courseId`, `blocks`), products: willThrow.map({ StubProduct.throw($0) })) } @@ -2204,12 +2232,12 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { willProduce(stubber) return given } - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willThrow: Error...) -> MethodStub { - return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) + public static func cancelAllDownloading(willThrow: Error...) -> MethodStub { + return Given(method: .m_cancelAllDownloading, products: willThrow.map({ StubProduct.throw($0) })) } - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willProduce: (StubberThrows) -> Void) -> MethodStub { + public static func cancelAllDownloading(willProduce: (StubberThrows) -> Void) -> MethodStub { let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) }() + let given: Given = { return Given(method: .m_cancelAllDownloading, products: willThrow.map({ StubProduct.throw($0) })) }() let stubber = given.stubThrows(for: (Void).self) willProduce(stubber) return given @@ -2231,17 +2259,18 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func publisher() -> Verify { return Verify(method: .m_publisher)} public static func eventPublisher() -> Verify { return Verify(method: .m_eventPublisher)} + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_addToDownloadQueue__blocks_blocks(`blocks`))} public static func getDownloadTasks() -> Verify { return Verify(method: .m_getDownloadTasks)} public static func getDownloadTasksForCourse(_ courseId: Parameter) -> Verify { return Verify(method: .m_getDownloadTasksForCourse__courseId(`courseId`))} public static func cancelDownloading(courseId: Parameter, blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_cancelDownloading__courseId_courseIdblocks_blocks(`courseId`, `blocks`))} public static func cancelDownloading(task: Parameter) -> Verify { return Verify(method: .m_cancelDownloading__task_task(`task`))} public static func cancelDownloading(courseId: Parameter) -> Verify { return Verify(method: .m_cancelDownloading__courseId_courseId(`courseId`))} + public static func cancelAllDownloading() -> Verify { return Verify(method: .m_cancelAllDownloading)} public static func deleteFile(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_deleteFile__blocks_blocks(`blocks`))} public static func deleteAllFiles() -> Verify { return Verify(method: .m_deleteAllFiles)} public static func fileUrl(for blockId: Parameter) -> Verify { return Verify(method: .m_fileUrl__for_blockId(`blockId`))} - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_addToDownloadQueue__blocks_blocks(`blocks`))} - public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_isLargeVideosSize__blocks_blocks(`blocks`))} public static func resumeDownloading() -> Verify { return Verify(method: .m_resumeDownloading)} + public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_isLargeVideosSize__blocks_blocks(`blocks`))} public static var currentDownloadTask: Verify { return Verify(method: .p_currentDownloadTask_get) } } @@ -2255,6 +2284,9 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func eventPublisher(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_eventPublisher, performs: perform) } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { + return Perform(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), performs: perform) + } public static func getDownloadTasks(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_getDownloadTasks, performs: perform) } @@ -2270,6 +2302,9 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func cancelDownloading(courseId: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_cancelDownloading__courseId_courseId(`courseId`), performs: perform) } + public static func cancelAllDownloading(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_cancelAllDownloading, performs: perform) + } public static func deleteFile(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { return Perform(method: .m_deleteFile__blocks_blocks(`blocks`), performs: perform) } @@ -2279,14 +2314,257 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func fileUrl(for blockId: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_fileUrl__for_blockId(`blockId`), performs: perform) } - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { - return Perform(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), performs: perform) + public static func resumeDownloading(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_resumeDownloading, performs: perform) } public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { return Perform(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), performs: perform) } - public static func resumeDownloading(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_resumeDownloading, performs: perform) + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - WebviewCookiesUpdateProtocol + +open class WebviewCookiesUpdateProtocolMock: WebviewCookiesUpdateProtocol, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + public var authInteractor: AuthInteractorProtocol { + get { invocations.append(.p_authInteractor_get); return __p_authInteractor ?? givenGetterValue(.p_authInteractor_get, "WebviewCookiesUpdateProtocolMock - stub value for authInteractor was not defined") } + } + private var __p_authInteractor: (AuthInteractorProtocol)? + + public var cookiesReady: Bool { + get { invocations.append(.p_cookiesReady_get); return __p_cookiesReady ?? givenGetterValue(.p_cookiesReady_get, "WebviewCookiesUpdateProtocolMock - stub value for cookiesReady was not defined") } + set { invocations.append(.p_cookiesReady_set(.value(newValue))); __p_cookiesReady = newValue } + } + private var __p_cookiesReady: (Bool)? + + public var updatingCookies: Bool { + get { invocations.append(.p_updatingCookies_get); return __p_updatingCookies ?? givenGetterValue(.p_updatingCookies_get, "WebviewCookiesUpdateProtocolMock - stub value for updatingCookies was not defined") } + set { invocations.append(.p_updatingCookies_set(.value(newValue))); __p_updatingCookies = newValue } + } + private var __p_updatingCookies: (Bool)? + + public var errorMessage: String? { + get { invocations.append(.p_errorMessage_get); return __p_errorMessage ?? optionalGivenGetterValue(.p_errorMessage_get, "WebviewCookiesUpdateProtocolMock - stub value for errorMessage was not defined") } + set { invocations.append(.p_errorMessage_set(.value(newValue))); __p_errorMessage = newValue } + } + private var __p_errorMessage: (String)? + + + + + + open func updateCookies(force: Bool, retryCount: Int) { + addInvocation(.m_updateCookies__force_forceretryCount_retryCount(Parameter.value(`force`), Parameter.value(`retryCount`))) + let perform = methodPerformValue(.m_updateCookies__force_forceretryCount_retryCount(Parameter.value(`force`), Parameter.value(`retryCount`))) as? (Bool, Int) -> Void + perform?(`force`, `retryCount`) + } + + + fileprivate enum MethodType { + case m_updateCookies__force_forceretryCount_retryCount(Parameter, Parameter) + case p_authInteractor_get + case p_cookiesReady_get + case p_cookiesReady_set(Parameter) + case p_updatingCookies_get + case p_updatingCookies_set(Parameter) + case p_errorMessage_get + case p_errorMessage_set(Parameter) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_updateCookies__force_forceretryCount_retryCount(let lhsForce, let lhsRetrycount), .m_updateCookies__force_forceretryCount_retryCount(let rhsForce, let rhsRetrycount)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsForce, rhs: rhsForce, with: matcher), lhsForce, rhsForce, "force")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsRetrycount, rhs: rhsRetrycount, with: matcher), lhsRetrycount, rhsRetrycount, "retryCount")) + return Matcher.ComparisonResult(results) + case (.p_authInteractor_get,.p_authInteractor_get): return Matcher.ComparisonResult.match + case (.p_cookiesReady_get,.p_cookiesReady_get): return Matcher.ComparisonResult.match + case (.p_cookiesReady_set(let left),.p_cookiesReady_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_updatingCookies_get,.p_updatingCookies_get): return Matcher.ComparisonResult.match + case (.p_updatingCookies_set(let left),.p_updatingCookies_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_errorMessage_get,.p_errorMessage_get): return Matcher.ComparisonResult.match + case (.p_errorMessage_set(let left),.p_errorMessage_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + default: return .none + } + } + + func intValue() -> Int { + switch self { + case let .m_updateCookies__force_forceretryCount_retryCount(p0, p1): return p0.intValue + p1.intValue + case .p_authInteractor_get: return 0 + case .p_cookiesReady_get: return 0 + case .p_cookiesReady_set(let newValue): return newValue.intValue + case .p_updatingCookies_get: return 0 + case .p_updatingCookies_set(let newValue): return newValue.intValue + case .p_errorMessage_get: return 0 + case .p_errorMessage_set(let newValue): return newValue.intValue + } + } + func assertionName() -> String { + switch self { + case .m_updateCookies__force_forceretryCount_retryCount: return ".updateCookies(force:retryCount:)" + case .p_authInteractor_get: return "[get] .authInteractor" + case .p_cookiesReady_get: return "[get] .cookiesReady" + case .p_cookiesReady_set: return "[set] .cookiesReady" + case .p_updatingCookies_get: return "[get] .updatingCookies" + case .p_updatingCookies_set: return "[set] .updatingCookies" + case .p_errorMessage_get: return "[get] .errorMessage" + case .p_errorMessage_set: return "[set] .errorMessage" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + public static func authInteractor(getter defaultValue: AuthInteractorProtocol...) -> PropertyStub { + return Given(method: .p_authInteractor_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func cookiesReady(getter defaultValue: Bool...) -> PropertyStub { + return Given(method: .p_cookiesReady_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func updatingCookies(getter defaultValue: Bool...) -> PropertyStub { + return Given(method: .p_updatingCookies_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func errorMessage(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_errorMessage_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + + } + + public struct Verify { + fileprivate var method: MethodType + + public static func updateCookies(force: Parameter, retryCount: Parameter) -> Verify { return Verify(method: .m_updateCookies__force_forceretryCount_retryCount(`force`, `retryCount`))} + public static var authInteractor: Verify { return Verify(method: .p_authInteractor_get) } + public static var cookiesReady: Verify { return Verify(method: .p_cookiesReady_get) } + public static func cookiesReady(set newValue: Parameter) -> Verify { return Verify(method: .p_cookiesReady_set(newValue)) } + public static var updatingCookies: Verify { return Verify(method: .p_updatingCookies_get) } + public static func updatingCookies(set newValue: Parameter) -> Verify { return Verify(method: .p_updatingCookies_set(newValue)) } + public static var errorMessage: Verify { return Verify(method: .p_errorMessage_get) } + public static func errorMessage(set newValue: Parameter) -> Verify { return Verify(method: .p_errorMessage_set(newValue)) } + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + public static func updateCookies(force: Parameter, retryCount: Parameter, perform: @escaping (Bool, Int) -> Void) -> Perform { + return Perform(method: .m_updateCookies__force_forceretryCount_retryCount(`force`, `retryCount`), performs: perform) } } diff --git a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsView.swift b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsView.swift index 5b9d946fa..e242b58d9 100644 --- a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsView.swift +++ b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsView.swift @@ -150,7 +150,7 @@ public struct DiscussionTopicsView: View { } .navigationBarHidden(false) .navigationBarBackButtonHidden(false) - .navigationTitle(DiscussionLocalization.title) + .navigationTitle(viewModel.title) .background( Theme.Colors.background .ignoresSafeArea() diff --git a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsViewModel.swift b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsViewModel.swift index 85ec229e8..0366e7a44 100644 --- a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsViewModel.swift +++ b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsViewModel.swift @@ -17,7 +17,7 @@ public class DiscussionTopicsViewModel: ObservableObject { @Published var showError: Bool = false @Published var discussionTopics: [DiscussionTopic]? @Published var courseID: String = "" - private var title: String + let title: String var errorMessage: String? { didSet { diff --git a/Discussion/DiscussionTests/DiscussionMock.generated.swift b/Discussion/DiscussionTests/DiscussionMock.generated.swift index fad4812d4..336d50a14 100644 --- a/Discussion/DiscussionTests/DiscussionMock.generated.swift +++ b/Discussion/DiscussionTests/DiscussionMock.generated.swift @@ -2679,6 +2679,19 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { return __value } + open func addToDownloadQueue(blocks: [CourseBlock]) throws { + addInvocation(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) + let perform = methodPerformValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void + perform?(`blocks`) + do { + _ = try methodReturnValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + open func getDownloadTasks() -> [DownloadDataTask] { addInvocation(.m_getDownloadTasks) let perform = methodPerformValue(.m_getDownloadTasks) as? () -> Void @@ -2746,6 +2759,19 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { } } + open func cancelAllDownloading() throws { + addInvocation(.m_cancelAllDownloading) + let perform = methodPerformValue(.m_cancelAllDownloading) as? () -> Void + perform?() + do { + _ = try methodReturnValue(.m_cancelAllDownloading).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + open func deleteFile(blocks: [CourseBlock]) { addInvocation(.m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) let perform = methodPerformValue(.m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void @@ -2771,12 +2797,12 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { return __value } - open func addToDownloadQueue(blocks: [CourseBlock]) throws { - addInvocation(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) - let perform = methodPerformValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void - perform?(`blocks`) + open func resumeDownloading() throws { + addInvocation(.m_resumeDownloading) + let perform = methodPerformValue(.m_resumeDownloading) as? () -> Void + perform?() do { - _ = try methodReturnValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))).casted() as Void + _ = try methodReturnValue(.m_resumeDownloading).casted() as Void } catch MockError.notStubed { // do nothing } catch { @@ -2798,34 +2824,22 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { return __value } - open func resumeDownloading() throws { - addInvocation(.m_resumeDownloading) - let perform = methodPerformValue(.m_resumeDownloading) as? () -> Void - perform?() - do { - _ = try methodReturnValue(.m_resumeDownloading).casted() as Void - } catch MockError.notStubed { - // do nothing - } catch { - throw error - } - } - fileprivate enum MethodType { case m_publisher case m_eventPublisher + case m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>) case m_getDownloadTasks case m_getDownloadTasksForCourse__courseId(Parameter) case m_cancelDownloading__courseId_courseIdblocks_blocks(Parameter, Parameter<[CourseBlock]>) case m_cancelDownloading__task_task(Parameter) case m_cancelDownloading__courseId_courseId(Parameter) + case m_cancelAllDownloading case m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>) case m_deleteAllFiles case m_fileUrl__for_blockId(Parameter) - case m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>) - case m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>) case m_resumeDownloading + case m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>) case p_currentDownloadTask_get static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { @@ -2834,6 +2848,11 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { case (.m_eventPublisher, .m_eventPublisher): return .match + case (.m_addToDownloadQueue__blocks_blocks(let lhsBlocks), .m_addToDownloadQueue__blocks_blocks(let rhsBlocks)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) + return Matcher.ComparisonResult(results) + case (.m_getDownloadTasks, .m_getDownloadTasks): return .match case (.m_getDownloadTasksForCourse__courseId(let lhsCourseid), .m_getDownloadTasksForCourse__courseId(let rhsCourseid)): @@ -2857,6 +2876,8 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) return Matcher.ComparisonResult(results) + case (.m_cancelAllDownloading, .m_cancelAllDownloading): return .match + case (.m_deleteFile__blocks_blocks(let lhsBlocks), .m_deleteFile__blocks_blocks(let rhsBlocks)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) @@ -2869,17 +2890,12 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockId")) return Matcher.ComparisonResult(results) - case (.m_addToDownloadQueue__blocks_blocks(let lhsBlocks), .m_addToDownloadQueue__blocks_blocks(let rhsBlocks)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) - return Matcher.ComparisonResult(results) + case (.m_resumeDownloading, .m_resumeDownloading): return .match case (.m_isLargeVideosSize__blocks_blocks(let lhsBlocks), .m_isLargeVideosSize__blocks_blocks(let rhsBlocks)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) return Matcher.ComparisonResult(results) - - case (.m_resumeDownloading, .m_resumeDownloading): return .match case (.p_currentDownloadTask_get,.p_currentDownloadTask_get): return Matcher.ComparisonResult.match default: return .none } @@ -2889,17 +2905,18 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { switch self { case .m_publisher: return 0 case .m_eventPublisher: return 0 + case let .m_addToDownloadQueue__blocks_blocks(p0): return p0.intValue case .m_getDownloadTasks: return 0 case let .m_getDownloadTasksForCourse__courseId(p0): return p0.intValue case let .m_cancelDownloading__courseId_courseIdblocks_blocks(p0, p1): return p0.intValue + p1.intValue case let .m_cancelDownloading__task_task(p0): return p0.intValue case let .m_cancelDownloading__courseId_courseId(p0): return p0.intValue + case .m_cancelAllDownloading: return 0 case let .m_deleteFile__blocks_blocks(p0): return p0.intValue case .m_deleteAllFiles: return 0 case let .m_fileUrl__for_blockId(p0): return p0.intValue - case let .m_addToDownloadQueue__blocks_blocks(p0): return p0.intValue - case let .m_isLargeVideosSize__blocks_blocks(p0): return p0.intValue case .m_resumeDownloading: return 0 + case let .m_isLargeVideosSize__blocks_blocks(p0): return p0.intValue case .p_currentDownloadTask_get: return 0 } } @@ -2907,17 +2924,18 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { switch self { case .m_publisher: return ".publisher()" case .m_eventPublisher: return ".eventPublisher()" + case .m_addToDownloadQueue__blocks_blocks: return ".addToDownloadQueue(blocks:)" case .m_getDownloadTasks: return ".getDownloadTasks()" case .m_getDownloadTasksForCourse__courseId: return ".getDownloadTasksForCourse(_:)" case .m_cancelDownloading__courseId_courseIdblocks_blocks: return ".cancelDownloading(courseId:blocks:)" case .m_cancelDownloading__task_task: return ".cancelDownloading(task:)" case .m_cancelDownloading__courseId_courseId: return ".cancelDownloading(courseId:)" + case .m_cancelAllDownloading: return ".cancelAllDownloading()" case .m_deleteFile__blocks_blocks: return ".deleteFile(blocks:)" case .m_deleteAllFiles: return ".deleteAllFiles()" case .m_fileUrl__for_blockId: return ".fileUrl(for:)" - case .m_addToDownloadQueue__blocks_blocks: return ".addToDownloadQueue(blocks:)" - case .m_isLargeVideosSize__blocks_blocks: return ".isLargeVideosSize(blocks:)" case .m_resumeDownloading: return ".resumeDownloading()" + case .m_isLargeVideosSize__blocks_blocks: return ".isLargeVideosSize(blocks:)" case .p_currentDownloadTask_get: return "[get] .currentDownloadTask" } } @@ -2995,6 +3013,16 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { willProduce(stubber) return given } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willThrow: Error...) -> MethodStub { + return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } public static func cancelDownloading(courseId: Parameter, blocks: Parameter<[CourseBlock]>, willThrow: Error...) -> MethodStub { return Given(method: .m_cancelDownloading__courseId_courseIdblocks_blocks(`courseId`, `blocks`), products: willThrow.map({ StubProduct.throw($0) })) } @@ -3025,12 +3053,12 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { willProduce(stubber) return given } - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willThrow: Error...) -> MethodStub { - return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) + public static func cancelAllDownloading(willThrow: Error...) -> MethodStub { + return Given(method: .m_cancelAllDownloading, products: willThrow.map({ StubProduct.throw($0) })) } - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willProduce: (StubberThrows) -> Void) -> MethodStub { + public static func cancelAllDownloading(willProduce: (StubberThrows) -> Void) -> MethodStub { let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) }() + let given: Given = { return Given(method: .m_cancelAllDownloading, products: willThrow.map({ StubProduct.throw($0) })) }() let stubber = given.stubThrows(for: (Void).self) willProduce(stubber) return given @@ -3052,17 +3080,18 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func publisher() -> Verify { return Verify(method: .m_publisher)} public static func eventPublisher() -> Verify { return Verify(method: .m_eventPublisher)} + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_addToDownloadQueue__blocks_blocks(`blocks`))} public static func getDownloadTasks() -> Verify { return Verify(method: .m_getDownloadTasks)} public static func getDownloadTasksForCourse(_ courseId: Parameter) -> Verify { return Verify(method: .m_getDownloadTasksForCourse__courseId(`courseId`))} public static func cancelDownloading(courseId: Parameter, blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_cancelDownloading__courseId_courseIdblocks_blocks(`courseId`, `blocks`))} public static func cancelDownloading(task: Parameter) -> Verify { return Verify(method: .m_cancelDownloading__task_task(`task`))} public static func cancelDownloading(courseId: Parameter) -> Verify { return Verify(method: .m_cancelDownloading__courseId_courseId(`courseId`))} + public static func cancelAllDownloading() -> Verify { return Verify(method: .m_cancelAllDownloading)} public static func deleteFile(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_deleteFile__blocks_blocks(`blocks`))} public static func deleteAllFiles() -> Verify { return Verify(method: .m_deleteAllFiles)} public static func fileUrl(for blockId: Parameter) -> Verify { return Verify(method: .m_fileUrl__for_blockId(`blockId`))} - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_addToDownloadQueue__blocks_blocks(`blocks`))} - public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_isLargeVideosSize__blocks_blocks(`blocks`))} public static func resumeDownloading() -> Verify { return Verify(method: .m_resumeDownloading)} + public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_isLargeVideosSize__blocks_blocks(`blocks`))} public static var currentDownloadTask: Verify { return Verify(method: .p_currentDownloadTask_get) } } @@ -3076,6 +3105,9 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func eventPublisher(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_eventPublisher, performs: perform) } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { + return Perform(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), performs: perform) + } public static func getDownloadTasks(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_getDownloadTasks, performs: perform) } @@ -3091,6 +3123,9 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func cancelDownloading(courseId: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_cancelDownloading__courseId_courseId(`courseId`), performs: perform) } + public static func cancelAllDownloading(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_cancelAllDownloading, performs: perform) + } public static func deleteFile(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { return Perform(method: .m_deleteFile__blocks_blocks(`blocks`), performs: perform) } @@ -3100,14 +3135,257 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func fileUrl(for blockId: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_fileUrl__for_blockId(`blockId`), performs: perform) } - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { - return Perform(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), performs: perform) + public static func resumeDownloading(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_resumeDownloading, performs: perform) } public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { return Perform(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), performs: perform) } - public static func resumeDownloading(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_resumeDownloading, performs: perform) + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - WebviewCookiesUpdateProtocol + +open class WebviewCookiesUpdateProtocolMock: WebviewCookiesUpdateProtocol, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + public var authInteractor: AuthInteractorProtocol { + get { invocations.append(.p_authInteractor_get); return __p_authInteractor ?? givenGetterValue(.p_authInteractor_get, "WebviewCookiesUpdateProtocolMock - stub value for authInteractor was not defined") } + } + private var __p_authInteractor: (AuthInteractorProtocol)? + + public var cookiesReady: Bool { + get { invocations.append(.p_cookiesReady_get); return __p_cookiesReady ?? givenGetterValue(.p_cookiesReady_get, "WebviewCookiesUpdateProtocolMock - stub value for cookiesReady was not defined") } + set { invocations.append(.p_cookiesReady_set(.value(newValue))); __p_cookiesReady = newValue } + } + private var __p_cookiesReady: (Bool)? + + public var updatingCookies: Bool { + get { invocations.append(.p_updatingCookies_get); return __p_updatingCookies ?? givenGetterValue(.p_updatingCookies_get, "WebviewCookiesUpdateProtocolMock - stub value for updatingCookies was not defined") } + set { invocations.append(.p_updatingCookies_set(.value(newValue))); __p_updatingCookies = newValue } + } + private var __p_updatingCookies: (Bool)? + + public var errorMessage: String? { + get { invocations.append(.p_errorMessage_get); return __p_errorMessage ?? optionalGivenGetterValue(.p_errorMessage_get, "WebviewCookiesUpdateProtocolMock - stub value for errorMessage was not defined") } + set { invocations.append(.p_errorMessage_set(.value(newValue))); __p_errorMessage = newValue } + } + private var __p_errorMessage: (String)? + + + + + + open func updateCookies(force: Bool, retryCount: Int) { + addInvocation(.m_updateCookies__force_forceretryCount_retryCount(Parameter.value(`force`), Parameter.value(`retryCount`))) + let perform = methodPerformValue(.m_updateCookies__force_forceretryCount_retryCount(Parameter.value(`force`), Parameter.value(`retryCount`))) as? (Bool, Int) -> Void + perform?(`force`, `retryCount`) + } + + + fileprivate enum MethodType { + case m_updateCookies__force_forceretryCount_retryCount(Parameter, Parameter) + case p_authInteractor_get + case p_cookiesReady_get + case p_cookiesReady_set(Parameter) + case p_updatingCookies_get + case p_updatingCookies_set(Parameter) + case p_errorMessage_get + case p_errorMessage_set(Parameter) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_updateCookies__force_forceretryCount_retryCount(let lhsForce, let lhsRetrycount), .m_updateCookies__force_forceretryCount_retryCount(let rhsForce, let rhsRetrycount)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsForce, rhs: rhsForce, with: matcher), lhsForce, rhsForce, "force")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsRetrycount, rhs: rhsRetrycount, with: matcher), lhsRetrycount, rhsRetrycount, "retryCount")) + return Matcher.ComparisonResult(results) + case (.p_authInteractor_get,.p_authInteractor_get): return Matcher.ComparisonResult.match + case (.p_cookiesReady_get,.p_cookiesReady_get): return Matcher.ComparisonResult.match + case (.p_cookiesReady_set(let left),.p_cookiesReady_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_updatingCookies_get,.p_updatingCookies_get): return Matcher.ComparisonResult.match + case (.p_updatingCookies_set(let left),.p_updatingCookies_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_errorMessage_get,.p_errorMessage_get): return Matcher.ComparisonResult.match + case (.p_errorMessage_set(let left),.p_errorMessage_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + default: return .none + } + } + + func intValue() -> Int { + switch self { + case let .m_updateCookies__force_forceretryCount_retryCount(p0, p1): return p0.intValue + p1.intValue + case .p_authInteractor_get: return 0 + case .p_cookiesReady_get: return 0 + case .p_cookiesReady_set(let newValue): return newValue.intValue + case .p_updatingCookies_get: return 0 + case .p_updatingCookies_set(let newValue): return newValue.intValue + case .p_errorMessage_get: return 0 + case .p_errorMessage_set(let newValue): return newValue.intValue + } + } + func assertionName() -> String { + switch self { + case .m_updateCookies__force_forceretryCount_retryCount: return ".updateCookies(force:retryCount:)" + case .p_authInteractor_get: return "[get] .authInteractor" + case .p_cookiesReady_get: return "[get] .cookiesReady" + case .p_cookiesReady_set: return "[set] .cookiesReady" + case .p_updatingCookies_get: return "[get] .updatingCookies" + case .p_updatingCookies_set: return "[set] .updatingCookies" + case .p_errorMessage_get: return "[get] .errorMessage" + case .p_errorMessage_set: return "[set] .errorMessage" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + public static func authInteractor(getter defaultValue: AuthInteractorProtocol...) -> PropertyStub { + return Given(method: .p_authInteractor_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func cookiesReady(getter defaultValue: Bool...) -> PropertyStub { + return Given(method: .p_cookiesReady_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func updatingCookies(getter defaultValue: Bool...) -> PropertyStub { + return Given(method: .p_updatingCookies_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func errorMessage(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_errorMessage_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + + } + + public struct Verify { + fileprivate var method: MethodType + + public static func updateCookies(force: Parameter, retryCount: Parameter) -> Verify { return Verify(method: .m_updateCookies__force_forceretryCount_retryCount(`force`, `retryCount`))} + public static var authInteractor: Verify { return Verify(method: .p_authInteractor_get) } + public static var cookiesReady: Verify { return Verify(method: .p_cookiesReady_get) } + public static func cookiesReady(set newValue: Parameter) -> Verify { return Verify(method: .p_cookiesReady_set(newValue)) } + public static var updatingCookies: Verify { return Verify(method: .p_updatingCookies_get) } + public static func updatingCookies(set newValue: Parameter) -> Verify { return Verify(method: .p_updatingCookies_set(newValue)) } + public static var errorMessage: Verify { return Verify(method: .p_errorMessage_get) } + public static func errorMessage(set newValue: Parameter) -> Verify { return Verify(method: .p_errorMessage_set(newValue)) } + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + public static func updateCookies(force: Parameter, retryCount: Parameter, perform: @escaping (Bool, Int) -> Void) -> Perform { + return Perform(method: .m_updateCookies__force_forceretryCount_retryCount(`force`, `retryCount`), performs: perform) } } diff --git a/OpenEdX/DI/ScreenAssembly.swift b/OpenEdX/DI/ScreenAssembly.swift index 9c61fc679..74074812d 100644 --- a/OpenEdX/DI/ScreenAssembly.swift +++ b/OpenEdX/DI/ScreenAssembly.swift @@ -186,7 +186,8 @@ class ScreenAssembly: Assembly { } container.register(ProfileViewModel.self) { r in ProfileViewModel( - interactor: r.resolve(ProfileInteractorProtocol.self)!, + interactor: r.resolve(ProfileInteractorProtocol.self)!, + downloadManager: r.resolve(DownloadManagerProtocol.self)!, router: r.resolve(ProfileRouter.self)!, analytics: r.resolve(ProfileAnalytics.self)!, config: r.resolve(ConfigProtocol.self)!, diff --git a/OpenEdX/Data/CorePersistence.swift b/OpenEdX/Data/CorePersistence.swift index 1070011dd..8af067d07 100644 --- a/OpenEdX/Data/CorePersistence.swift +++ b/OpenEdX/Data/CorePersistence.swift @@ -12,74 +12,68 @@ import Combine public class CorePersistence: CorePersistenceProtocol { - private var context: NSManagedObjectContext + // MARK: - Predicate - public init(context: NSManagedObjectContext) { - self.context = context - } + enum CDPredicate { + case id(String) + case courseId(String) + case state(String) - public func publisher() -> AnyPublisher { - let notification = NSManagedObjectContext.didChangeObjectsNotification - return NotificationCenter.default.publisher(for: notification, object: context) - .compactMap({ notification in - guard let userInfo = notification.userInfo else { return nil } + var predicate: NSPredicate { + switch self { + case .id(let id): + NSPredicate(format: "id = %@", id) + case .courseId(let courseId): + NSPredicate(format: "courseId = %@", courseId) + case .state(let state): + NSPredicate(format: "state != %@", state) + } + } + } - if let inserts = userInfo[NSInsertedObjectsKey] as? Set, inserts.count > 0 { - return inserts.count - } + // MARK: - Properties - if let updates = userInfo[NSUpdatedObjectsKey] as? Set, updates.count > 0 { - return updates.count - } + private var context: NSManagedObjectContext + private var userId: Int? - if let deletes = userInfo[NSDeletedObjectsKey] as? Set, deletes.count > 0 { - return deletes.count - } + public init(context: NSManagedObjectContext) { + self.context = context + } - return nil - }) - .eraseToAnyPublisher() + public func set(userId: Int) { + self.userId = userId } - public func getDownloadDataTasks(completion: @escaping ([DownloadDataTask]) -> Void) { - context.performAndWait { - let request = CDDownloadData.fetchRequest() - guard let downloadData = try? context.fetch(request) else { - completion([]) - return - } - let downloads = downloadData.map { - DownloadDataTask( - id: $0.id ?? "", - courseId: $0.courseId ?? "", - url: $0.url ?? "", - fileName: $0.fileName ?? "", - displayName: $0.displayName ?? "", - progress: $0.progress, - resumeData: $0.resumeData, - state: DownloadState(rawValue: $0.state ?? "") ?? .waiting, - type: DownloadType(rawValue: $0.type ?? "") ?? .video, - fileSize: Int($0.fileSize) - ) - } - completion(downloads) - } + public func getUserID() -> Int? { + userId } - public func addToDownloadQueue(blocks: [CourseBlock], downloadQuality: DownloadQuality) { + // MARK: - Public Intents + + public func addToDownloadQueue( + blocks: [CourseBlock], + downloadQuality: DownloadQuality + ) { for block in blocks { - let request = CDDownloadData.fetchRequest() - request.predicate = NSPredicate(format: "id = %@", block.id) - guard (try? context.fetch(request).first) == nil else { continue } + let downloadDataId = downloadDataId(from: block.id) + + let data = try? fetchCDDownloadData( + predicate: CDPredicate.id(downloadDataId) + ) + guard data?.first == nil else { continue } + guard let video = block.encodedVideo?.video(downloadQuality: downloadQuality), let url = video.url, let fileExtension = URL(string: url)?.pathExtension else { continue } + let fileName = "\(block.id).\(fileExtension)" context.performAndWait { let newDownloadData = CDDownloadData(context: context) context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump - newDownloadData.id = block.id + newDownloadData.id = downloadDataId + newDownloadData.blockId = block.id + newDownloadData.userId = getUserId32() ?? 0 newDownloadData.courseId = block.courseId newDownloadData.url = url newDownloadData.fileName = fileName @@ -93,101 +87,105 @@ public class CorePersistence: CorePersistenceProtocol { } } - public func getNextBlockForDownloading() -> DownloadDataTask? { - let request = CDDownloadData.fetchRequest() - request.predicate = NSPredicate(format: "state != %@", DownloadState.finished.rawValue) - request.fetchLimit = 1 - guard let data = try? context.fetch(request).first else { return nil } - return DownloadDataTask( - id: data.id ?? "", - courseId: data.courseId ?? "", - url: data.url ?? "", - fileName: data.fileName ?? "", - displayName: data.displayName ?? "", - progress: data.progress, - resumeData: data.resumeData, - state: DownloadState(rawValue: data.state ?? "") ?? .waiting, - type: DownloadType(rawValue: data.type ?? "" ) ?? .video, - fileSize: Int(data.fileSize) - ) + public func getDownloadDataTasks(completion: @escaping ([DownloadDataTask]) -> Void) { + context.performAndWait { + guard let data = try? fetchCDDownloadData() else { + completion([]) + return + } + + let downloads = data.downloadDataTasks() + + completion(downloads) + } } - public func getDownloadDataTasksForCourse(_ courseId: String, completion: @escaping ([DownloadDataTask]) -> Void) { + public func getDownloadDataTasksForCourse( + _ courseId: String, + completion: @escaping ([DownloadDataTask]) -> Void + ) { context.performAndWait { - let request = CDDownloadData.fetchRequest() - request.predicate = NSPredicate(format: "courseId = %@", courseId) - guard let downloadData = try? context.fetch(request) else { + guard let data = try? fetchCDDownloadData( + predicate: .courseId(courseId) + ) else { completion([]) return } - let downloads = downloadData.map { - DownloadDataTask( - id: $0.id ?? "", - courseId: $0.courseId ?? "", - url: $0.url ?? "", - fileName: $0.fileName ?? "", - displayName: $0.displayName ?? "", - progress: $0.progress, - resumeData: $0.resumeData, - state: DownloadState(rawValue: $0.state ?? "") ?? .waiting, - type: DownloadType(rawValue: $0.type ?? "") ?? .video, - fileSize: Int($0.fileSize) - ) + + if data.isEmpty { + completion([]) + return } + + let downloads = data + .downloadDataTasks() + .filter(userId: userId) + completion(downloads) } } - public func downloadDataTask(for blockId: String, completion: @escaping (DownloadDataTask?) -> Void) { + public func downloadDataTask( + for blockId: String, + completion: @escaping (DownloadDataTask?) -> Void + ) { context.performAndWait { - let request = CDDownloadData.fetchRequest() - request.predicate = NSPredicate(format: "id = %@", blockId) - guard let downloadData = try? context.fetch(request).first else { + let data = try? fetchCDDownloadData( + predicate: .id(downloadDataId(from: blockId)) + ) + + guard let downloadData = data?.first else { completion(nil) return } - let data = DownloadDataTask( - id: downloadData.id ?? "", - courseId: downloadData.courseId ?? "", - url: downloadData.url ?? "", - fileName: downloadData.fileName ?? "", - displayName: downloadData.displayName ?? "", - progress: downloadData.progress, - resumeData: downloadData.resumeData, - state: DownloadState(rawValue: downloadData.state ?? "") ?? .waiting, - type: DownloadType(rawValue: downloadData.type ?? "" ) ?? .video, - fileSize: Int(downloadData.fileSize) - ) - completion(data) + + let downloadDataTask = DownloadDataTask(sourse: downloadData) + + completion(downloadDataTask) } } public func downloadDataTask(for blockId: String) -> DownloadDataTask? { - let request = CDDownloadData.fetchRequest() - request.predicate = NSPredicate(format: "id = %@", blockId) - guard let downloadData = try? context.fetch(request).first else { return nil } - return DownloadDataTask( - id: downloadData.id ?? "", - courseId: downloadData.courseId ?? "", - url: downloadData.url ?? "", - fileName: downloadData.fileName ?? "", - displayName: downloadData.displayName ?? "", - progress: downloadData.progress, - resumeData: downloadData.resumeData, - state: DownloadState(rawValue: downloadData.state ?? "") ?? .waiting, - type: DownloadType(rawValue: downloadData.type ?? "" ) ?? .video, - fileSize: Int(downloadData.fileSize) + let data = try? fetchCDDownloadData( + predicate: .id(downloadDataId(from: blockId)) + ) + + guard let downloadData = data?.first else { return nil } + + return DownloadDataTask(sourse: downloadData) + } + + public func nextBlockForDownloading() -> DownloadDataTask? { + let data = try? fetchCDDownloadData( + predicate: .state(DownloadState.finished.rawValue), + fetchLimit: 1 ) + + guard let downloadData = data?.first else { + return nil + } + + return DownloadDataTask(sourse: downloadData) } - public func updateDownloadState(id: String, state: DownloadState, resumeData: Data?) { + public func updateDownloadState( + id: String, + state: DownloadState, + resumeData: Data? + ) { context.performAndWait { - let request = CDDownloadData.fetchRequest() - request.predicate = NSPredicate(format: "id = %@", id) - guard let downloadData = try? context.fetch(request).first else { return } - downloadData.state = state.rawValue - if state == .finished { downloadData.progress = 1 } - downloadData.resumeData = resumeData + guard let data = try? fetchCDDownloadData( + predicate: .id(downloadDataId(from: id)) + ) else { + return + } + + guard let task = data.first else { return } + + task.state = state.rawValue + if state == .finished { task.progress = 1 } + task.resumeData = resumeData + do { try context.save() } catch { @@ -198,33 +196,39 @@ public class CorePersistence: CorePersistenceProtocol { public func deleteDownloadDataTask(id: String) throws { context.performAndWait { - let request = CDDownloadData.fetchRequest() - request.predicate = NSPredicate(format: "id = %@", id) do { - let records = try context.fetch(request) + let records = try fetchCDDownloadData( + predicate: .id(downloadDataId(from: id)) + ) + for record in records { context.delete(record) try context.save() debugLog("File erased successfully") } + } catch { debugLog("Error fetching records: \(error.localizedDescription)") } } } - public func saveDownloadDataTask(data: DownloadDataTask) { + public func saveDownloadDataTask(_ task: DownloadDataTask) { context.performAndWait { let newDownloadData = CDDownloadData(context: context) context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump - newDownloadData.id = data.id - newDownloadData.courseId = data.courseId - newDownloadData.url = data.url - newDownloadData.progress = data.progress - newDownloadData.fileName = data.fileName - newDownloadData.resumeData = data.resumeData - newDownloadData.state = data.state.rawValue - newDownloadData.fileSize = Int32(data.fileSize) + newDownloadData.id = task.id + newDownloadData.blockId = task.blockId + newDownloadData.userId = Int32(task.userId) + newDownloadData.courseId = task.courseId + newDownloadData.url = task.url + newDownloadData.progress = task.progress + newDownloadData.fileName = task.fileName + newDownloadData.displayName = task.displayName + newDownloadData.resumeData = task.resumeData + newDownloadData.state = task.state.rawValue + newDownloadData.type = task.type.rawValue + newDownloadData.fileSize = Int32(task.fileSize) do { try context.save() @@ -233,4 +237,84 @@ public class CorePersistence: CorePersistenceProtocol { } } } + + public func publisher() -> AnyPublisher { + let notification = NSManagedObjectContext.didChangeObjectsNotification + return NotificationCenter.default.publisher(for: notification, object: context) + .compactMap({ notification in + guard let userInfo = notification.userInfo else { return nil } + + if let inserts = userInfo[NSInsertedObjectsKey] as? Set, inserts.count > 0 { + return inserts.count + } + + if let updates = userInfo[NSUpdatedObjectsKey] as? Set, updates.count > 0 { + return updates.count + } + + if let deletes = userInfo[NSDeletedObjectsKey] as? Set, deletes.count > 0 { + return deletes.count + } + + return nil + }) + .eraseToAnyPublisher() + } + + // MARK: - Private Intents + + private func fetchCDDownloadData( + predicate: CDPredicate? = nil, + fetchLimit: Int? = nil + ) throws -> [CDDownloadData] { + let request = CDDownloadData.fetchRequest() + if let predicate = predicate { + request.predicate = predicate.predicate + } + if let fetchLimit = fetchLimit { + request.fetchLimit = fetchLimit + } + let data = try context.fetch(request).filter { + guard let userId = getUserId32() else { + return true + } + debugLog(userId, "-userId-") + return $0.userId == userId + } + return data + } + + private func getUserId32() -> Int32? { + guard let userId else { + return nil + } + return Int32(userId) + } + + private func downloadDataId(from id: String) -> String { + guard let userId else { + return id + } + if id.contains(String(userId)) { + return id + } + return "\(userId)_\(id)" + } +} + +extension Array where Element == DownloadDataTask { + func filter(userId: Int?) -> [DownloadDataTask] { + filter { + guard let userId else { + return true + } + return $0.userId == userId + } + } +} + +extension Array where Element == CDDownloadData { + func downloadDataTasks() -> [DownloadDataTask] { + compactMap { DownloadDataTask(sourse: $0) } + } } diff --git a/OpenEdX/Router.swift b/OpenEdX/Router.swift index adc46d1f6..228a72e67 100644 --- a/OpenEdX/Router.swift +++ b/OpenEdX/Router.swift @@ -60,16 +60,22 @@ public class Router: AuthorizationRouter, public func showMainOrWhatsNewScreen(sourceScreen: LogistrationSourceScreen) { showToolBar() - var storage = Container.shared.resolve(WhatsNewStorage.self)! + var whatsNewStorage = Container.shared.resolve(WhatsNewStorage.self)! let config = Container.shared.resolve(ConfigProtocol.self)! + let persistence = Container.shared.resolve(CorePersistenceProtocol.self)! + let coreStorage = Container.shared.resolve(CoreStorage.self)! - let viewModel = WhatsNewViewModel(storage: storage, sourceScreen: sourceScreen) + if let userId = coreStorage.user?.id { + persistence.set(userId: userId) + } + + let viewModel = WhatsNewViewModel(storage: whatsNewStorage, sourceScreen: sourceScreen) let whatsNew = WhatsNewView(router: Container.shared.resolve(WhatsNewRouter.self)!, viewModel: viewModel) let shouldShowWhatsNew = viewModel.shouldShowWhatsNew() if shouldShowWhatsNew && config.features.whatNewEnabled { if let jsonVersion = viewModel.getVersion() { - storage.whatsNewVersion = jsonVersion + whatsNewStorage.whatsNewVersion = jsonVersion } let controller = UIHostingController(rootView: whatsNew) navigationController.viewControllers = [controller] diff --git a/Profile/Profile/Data/ProfileRepository.swift b/Profile/Profile/Data/ProfileRepository.swift index 83f37215a..7608ff849 100644 --- a/Profile/Profile/Data/ProfileRepository.swift +++ b/Profile/Profile/Data/ProfileRepository.swift @@ -71,8 +71,6 @@ public class ProfileRepository: ProfileRepositoryProtocol { ProfileEndpoint.logOut(refreshToken: refreshToken, clientID: config.oAuthClientId) ) storage.clear() - await downloadManager.deleteAllFiles() - coreDataHandler.clear() } public func getSpokenLanguages() -> [PickerFields.Option] { diff --git a/Profile/Profile/Presentation/Profile/ProfileView.swift b/Profile/Profile/Presentation/Profile/ProfileView.swift index 0450da9c8..e7e859228 100644 --- a/Profile/Profile/Presentation/Profile/ProfileView.swift +++ b/Profile/Profile/Presentation/Profile/ProfileView.swift @@ -237,11 +237,14 @@ public struct ProfileView: View { struct ProfileView_Previews: PreviewProvider { static var previews: some View { let router = ProfileRouterMock() - let vm = ProfileViewModel(interactor: ProfileInteractor.mock, - router: router, - analytics: ProfileAnalyticsMock(), - config: ConfigMock(), - connectivity: Connectivity()) + let vm = ProfileViewModel( + interactor: ProfileInteractor.mock, + downloadManager: DownloadManagerMock(), + router: router, + analytics: ProfileAnalyticsMock(), + config: ConfigMock(), + connectivity: Connectivity() + ) ProfileView(viewModel: vm, settingsTapped: .constant(false)) .preferredColorScheme(.light) diff --git a/Profile/Profile/Presentation/Profile/ProfileViewModel.swift b/Profile/Profile/Presentation/Profile/ProfileViewModel.swift index ae3d9a309..a502bd138 100644 --- a/Profile/Profile/Presentation/Profile/ProfileViewModel.swift +++ b/Profile/Profile/Presentation/Profile/ProfileViewModel.swift @@ -40,16 +40,19 @@ public class ProfileViewModel: ObservableObject { let connectivity: ConnectivityProtocol private let interactor: ProfileInteractorProtocol + private let downloadManager: DownloadManagerProtocol private let analytics: ProfileAnalytics public init( interactor: ProfileInteractorProtocol, + downloadManager: DownloadManagerProtocol, router: ProfileRouter, analytics: ProfileAnalytics, config: ConfigProtocol, connectivity: ConnectivityProtocol ) { self.interactor = interactor + self.downloadManager = downloadManager self.router = router self.analytics = analytics self.config = config @@ -120,6 +123,7 @@ public class ProfileViewModel: ObservableObject { @MainActor func logOut() async { try? await interactor.logOut() + try? await downloadManager.cancelAllDownloading() router.showStartupScreen() analytics.userLogout(force: false) } diff --git a/Profile/ProfileTests/Presentation/Profile/ProfileViewModelTests.swift b/Profile/ProfileTests/Presentation/Profile/ProfileViewModelTests.swift index c6888fcaf..40e56bbfa 100644 --- a/Profile/ProfileTests/Presentation/Profile/ProfileViewModelTests.swift +++ b/Profile/ProfileTests/Presentation/Profile/ProfileViewModelTests.swift @@ -92,6 +92,7 @@ final class ProfileViewModelTests: XCTestCase { let connectivity = ConnectivityProtocolMock() let viewModel = ProfileViewModel( interactor: interactor, + downloadManager: DownloadManagerMock(), router: router, analytics: analytics, config: ConfigMock(), @@ -130,6 +131,7 @@ final class ProfileViewModelTests: XCTestCase { let connectivity = ConnectivityProtocolMock() let viewModel = ProfileViewModel( interactor: interactor, + downloadManager: DownloadManagerMock(), router: router, analytics: analytics, config: ConfigMock(), @@ -167,6 +169,7 @@ final class ProfileViewModelTests: XCTestCase { let connectivity = ConnectivityProtocolMock() let viewModel = ProfileViewModel( interactor: interactor, + downloadManager: DownloadManagerMock(), router: router, analytics: analytics, config: ConfigMock(), @@ -206,6 +209,7 @@ final class ProfileViewModelTests: XCTestCase { let connectivity = ConnectivityProtocolMock() let viewModel = ProfileViewModel( interactor: interactor, + downloadManager: DownloadManagerMock(), router: router, analytics: analytics, config: ConfigMock(), @@ -231,6 +235,7 @@ final class ProfileViewModelTests: XCTestCase { let connectivity = ConnectivityProtocolMock() let viewModel = ProfileViewModel( interactor: interactor, + downloadManager: DownloadManagerMock(), router: router, analytics: analytics, config: ConfigMock(), @@ -252,6 +257,7 @@ final class ProfileViewModelTests: XCTestCase { let connectivity = ConnectivityProtocolMock() let viewModel = ProfileViewModel( interactor: interactor, + downloadManager: DownloadManagerMock(), router: router, analytics: analytics, config: ConfigMock(), @@ -269,7 +275,8 @@ final class ProfileViewModelTests: XCTestCase { let analytics = ProfileAnalyticsMock() let connectivity = ConnectivityProtocolMock() let viewModel = ProfileViewModel( - interactor: interactor, + interactor: interactor, + downloadManager: DownloadManagerMock(), router: router, analytics: analytics, config: ConfigMock(), @@ -287,7 +294,8 @@ final class ProfileViewModelTests: XCTestCase { let analytics = ProfileAnalyticsMock() let connectivity = ConnectivityProtocolMock() let viewModel = ProfileViewModel( - interactor: interactor, + interactor: interactor, + downloadManager: DownloadManagerMock(), router: router, analytics: analytics, config: ConfigMock(), @@ -305,7 +313,8 @@ final class ProfileViewModelTests: XCTestCase { let analytics = ProfileAnalyticsMock() let connectivity = ConnectivityProtocolMock() let viewModel = ProfileViewModel( - interactor: interactor, + interactor: interactor, + downloadManager: DownloadManagerMock(), router: router, analytics: analytics, config: ConfigMock(), @@ -323,7 +332,8 @@ final class ProfileViewModelTests: XCTestCase { let analytics = ProfileAnalyticsMock() let connectivity = ConnectivityProtocolMock() let viewModel = ProfileViewModel( - interactor: interactor, + interactor: interactor, + downloadManager: DownloadManagerMock(), router: router, analytics: analytics, config: ConfigMock(), diff --git a/Profile/ProfileTests/ProfileMock.generated.swift b/Profile/ProfileTests/ProfileMock.generated.swift index 047a50d8d..19f41b288 100644 --- a/Profile/ProfileTests/ProfileMock.generated.swift +++ b/Profile/ProfileTests/ProfileMock.generated.swift @@ -1190,6 +1190,19 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { return __value } + open func addToDownloadQueue(blocks: [CourseBlock]) throws { + addInvocation(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) + let perform = methodPerformValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void + perform?(`blocks`) + do { + _ = try methodReturnValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + open func getDownloadTasks() -> [DownloadDataTask] { addInvocation(.m_getDownloadTasks) let perform = methodPerformValue(.m_getDownloadTasks) as? () -> Void @@ -1257,6 +1270,19 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { } } + open func cancelAllDownloading() throws { + addInvocation(.m_cancelAllDownloading) + let perform = methodPerformValue(.m_cancelAllDownloading) as? () -> Void + perform?() + do { + _ = try methodReturnValue(.m_cancelAllDownloading).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + open func deleteFile(blocks: [CourseBlock]) { addInvocation(.m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) let perform = methodPerformValue(.m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void @@ -1282,12 +1308,12 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { return __value } - open func addToDownloadQueue(blocks: [CourseBlock]) throws { - addInvocation(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) - let perform = methodPerformValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void - perform?(`blocks`) + open func resumeDownloading() throws { + addInvocation(.m_resumeDownloading) + let perform = methodPerformValue(.m_resumeDownloading) as? () -> Void + perform?() do { - _ = try methodReturnValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))).casted() as Void + _ = try methodReturnValue(.m_resumeDownloading).casted() as Void } catch MockError.notStubed { // do nothing } catch { @@ -1309,34 +1335,22 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { return __value } - open func resumeDownloading() throws { - addInvocation(.m_resumeDownloading) - let perform = methodPerformValue(.m_resumeDownloading) as? () -> Void - perform?() - do { - _ = try methodReturnValue(.m_resumeDownloading).casted() as Void - } catch MockError.notStubed { - // do nothing - } catch { - throw error - } - } - fileprivate enum MethodType { case m_publisher case m_eventPublisher + case m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>) case m_getDownloadTasks case m_getDownloadTasksForCourse__courseId(Parameter) case m_cancelDownloading__courseId_courseIdblocks_blocks(Parameter, Parameter<[CourseBlock]>) case m_cancelDownloading__task_task(Parameter) case m_cancelDownloading__courseId_courseId(Parameter) + case m_cancelAllDownloading case m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>) case m_deleteAllFiles case m_fileUrl__for_blockId(Parameter) - case m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>) - case m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>) case m_resumeDownloading + case m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>) case p_currentDownloadTask_get static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { @@ -1345,6 +1359,11 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { case (.m_eventPublisher, .m_eventPublisher): return .match + case (.m_addToDownloadQueue__blocks_blocks(let lhsBlocks), .m_addToDownloadQueue__blocks_blocks(let rhsBlocks)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) + return Matcher.ComparisonResult(results) + case (.m_getDownloadTasks, .m_getDownloadTasks): return .match case (.m_getDownloadTasksForCourse__courseId(let lhsCourseid), .m_getDownloadTasksForCourse__courseId(let rhsCourseid)): @@ -1368,6 +1387,8 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) return Matcher.ComparisonResult(results) + case (.m_cancelAllDownloading, .m_cancelAllDownloading): return .match + case (.m_deleteFile__blocks_blocks(let lhsBlocks), .m_deleteFile__blocks_blocks(let rhsBlocks)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) @@ -1380,17 +1401,12 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockId")) return Matcher.ComparisonResult(results) - case (.m_addToDownloadQueue__blocks_blocks(let lhsBlocks), .m_addToDownloadQueue__blocks_blocks(let rhsBlocks)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) - return Matcher.ComparisonResult(results) + case (.m_resumeDownloading, .m_resumeDownloading): return .match case (.m_isLargeVideosSize__blocks_blocks(let lhsBlocks), .m_isLargeVideosSize__blocks_blocks(let rhsBlocks)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) return Matcher.ComparisonResult(results) - - case (.m_resumeDownloading, .m_resumeDownloading): return .match case (.p_currentDownloadTask_get,.p_currentDownloadTask_get): return Matcher.ComparisonResult.match default: return .none } @@ -1400,17 +1416,18 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { switch self { case .m_publisher: return 0 case .m_eventPublisher: return 0 + case let .m_addToDownloadQueue__blocks_blocks(p0): return p0.intValue case .m_getDownloadTasks: return 0 case let .m_getDownloadTasksForCourse__courseId(p0): return p0.intValue case let .m_cancelDownloading__courseId_courseIdblocks_blocks(p0, p1): return p0.intValue + p1.intValue case let .m_cancelDownloading__task_task(p0): return p0.intValue case let .m_cancelDownloading__courseId_courseId(p0): return p0.intValue + case .m_cancelAllDownloading: return 0 case let .m_deleteFile__blocks_blocks(p0): return p0.intValue case .m_deleteAllFiles: return 0 case let .m_fileUrl__for_blockId(p0): return p0.intValue - case let .m_addToDownloadQueue__blocks_blocks(p0): return p0.intValue - case let .m_isLargeVideosSize__blocks_blocks(p0): return p0.intValue case .m_resumeDownloading: return 0 + case let .m_isLargeVideosSize__blocks_blocks(p0): return p0.intValue case .p_currentDownloadTask_get: return 0 } } @@ -1418,17 +1435,18 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { switch self { case .m_publisher: return ".publisher()" case .m_eventPublisher: return ".eventPublisher()" + case .m_addToDownloadQueue__blocks_blocks: return ".addToDownloadQueue(blocks:)" case .m_getDownloadTasks: return ".getDownloadTasks()" case .m_getDownloadTasksForCourse__courseId: return ".getDownloadTasksForCourse(_:)" case .m_cancelDownloading__courseId_courseIdblocks_blocks: return ".cancelDownloading(courseId:blocks:)" case .m_cancelDownloading__task_task: return ".cancelDownloading(task:)" case .m_cancelDownloading__courseId_courseId: return ".cancelDownloading(courseId:)" + case .m_cancelAllDownloading: return ".cancelAllDownloading()" case .m_deleteFile__blocks_blocks: return ".deleteFile(blocks:)" case .m_deleteAllFiles: return ".deleteAllFiles()" case .m_fileUrl__for_blockId: return ".fileUrl(for:)" - case .m_addToDownloadQueue__blocks_blocks: return ".addToDownloadQueue(blocks:)" - case .m_isLargeVideosSize__blocks_blocks: return ".isLargeVideosSize(blocks:)" case .m_resumeDownloading: return ".resumeDownloading()" + case .m_isLargeVideosSize__blocks_blocks: return ".isLargeVideosSize(blocks:)" case .p_currentDownloadTask_get: return "[get] .currentDownloadTask" } } @@ -1506,6 +1524,16 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { willProduce(stubber) return given } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willThrow: Error...) -> MethodStub { + return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } public static func cancelDownloading(courseId: Parameter, blocks: Parameter<[CourseBlock]>, willThrow: Error...) -> MethodStub { return Given(method: .m_cancelDownloading__courseId_courseIdblocks_blocks(`courseId`, `blocks`), products: willThrow.map({ StubProduct.throw($0) })) } @@ -1536,12 +1564,12 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { willProduce(stubber) return given } - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willThrow: Error...) -> MethodStub { - return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) + public static func cancelAllDownloading(willThrow: Error...) -> MethodStub { + return Given(method: .m_cancelAllDownloading, products: willThrow.map({ StubProduct.throw($0) })) } - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willProduce: (StubberThrows) -> Void) -> MethodStub { + public static func cancelAllDownloading(willProduce: (StubberThrows) -> Void) -> MethodStub { let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) }() + let given: Given = { return Given(method: .m_cancelAllDownloading, products: willThrow.map({ StubProduct.throw($0) })) }() let stubber = given.stubThrows(for: (Void).self) willProduce(stubber) return given @@ -1563,17 +1591,18 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func publisher() -> Verify { return Verify(method: .m_publisher)} public static func eventPublisher() -> Verify { return Verify(method: .m_eventPublisher)} + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_addToDownloadQueue__blocks_blocks(`blocks`))} public static func getDownloadTasks() -> Verify { return Verify(method: .m_getDownloadTasks)} public static func getDownloadTasksForCourse(_ courseId: Parameter) -> Verify { return Verify(method: .m_getDownloadTasksForCourse__courseId(`courseId`))} public static func cancelDownloading(courseId: Parameter, blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_cancelDownloading__courseId_courseIdblocks_blocks(`courseId`, `blocks`))} public static func cancelDownloading(task: Parameter) -> Verify { return Verify(method: .m_cancelDownloading__task_task(`task`))} public static func cancelDownloading(courseId: Parameter) -> Verify { return Verify(method: .m_cancelDownloading__courseId_courseId(`courseId`))} + public static func cancelAllDownloading() -> Verify { return Verify(method: .m_cancelAllDownloading)} public static func deleteFile(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_deleteFile__blocks_blocks(`blocks`))} public static func deleteAllFiles() -> Verify { return Verify(method: .m_deleteAllFiles)} public static func fileUrl(for blockId: Parameter) -> Verify { return Verify(method: .m_fileUrl__for_blockId(`blockId`))} - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_addToDownloadQueue__blocks_blocks(`blocks`))} - public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_isLargeVideosSize__blocks_blocks(`blocks`))} public static func resumeDownloading() -> Verify { return Verify(method: .m_resumeDownloading)} + public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_isLargeVideosSize__blocks_blocks(`blocks`))} public static var currentDownloadTask: Verify { return Verify(method: .p_currentDownloadTask_get) } } @@ -1587,6 +1616,9 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func eventPublisher(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_eventPublisher, performs: perform) } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { + return Perform(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), performs: perform) + } public static func getDownloadTasks(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_getDownloadTasks, performs: perform) } @@ -1602,6 +1634,9 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func cancelDownloading(courseId: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_cancelDownloading__courseId_courseId(`courseId`), performs: perform) } + public static func cancelAllDownloading(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_cancelAllDownloading, performs: perform) + } public static func deleteFile(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { return Perform(method: .m_deleteFile__blocks_blocks(`blocks`), performs: perform) } @@ -1611,15 +1646,12 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func fileUrl(for blockId: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_fileUrl__for_blockId(`blockId`), performs: perform) } - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { - return Perform(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), performs: perform) + public static func resumeDownloading(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_resumeDownloading, performs: perform) } public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { return Perform(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), performs: perform) } - public static func resumeDownloading(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_resumeDownloading, performs: perform) - } } public func given(_ method: Given) { @@ -3086,3 +3118,249 @@ open class ProfileRouterMock: ProfileRouter, Mock { } } +// MARK: - WebviewCookiesUpdateProtocol + +open class WebviewCookiesUpdateProtocolMock: WebviewCookiesUpdateProtocol, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + public var authInteractor: AuthInteractorProtocol { + get { invocations.append(.p_authInteractor_get); return __p_authInteractor ?? givenGetterValue(.p_authInteractor_get, "WebviewCookiesUpdateProtocolMock - stub value for authInteractor was not defined") } + } + private var __p_authInteractor: (AuthInteractorProtocol)? + + public var cookiesReady: Bool { + get { invocations.append(.p_cookiesReady_get); return __p_cookiesReady ?? givenGetterValue(.p_cookiesReady_get, "WebviewCookiesUpdateProtocolMock - stub value for cookiesReady was not defined") } + set { invocations.append(.p_cookiesReady_set(.value(newValue))); __p_cookiesReady = newValue } + } + private var __p_cookiesReady: (Bool)? + + public var updatingCookies: Bool { + get { invocations.append(.p_updatingCookies_get); return __p_updatingCookies ?? givenGetterValue(.p_updatingCookies_get, "WebviewCookiesUpdateProtocolMock - stub value for updatingCookies was not defined") } + set { invocations.append(.p_updatingCookies_set(.value(newValue))); __p_updatingCookies = newValue } + } + private var __p_updatingCookies: (Bool)? + + public var errorMessage: String? { + get { invocations.append(.p_errorMessage_get); return __p_errorMessage ?? optionalGivenGetterValue(.p_errorMessage_get, "WebviewCookiesUpdateProtocolMock - stub value for errorMessage was not defined") } + set { invocations.append(.p_errorMessage_set(.value(newValue))); __p_errorMessage = newValue } + } + private var __p_errorMessage: (String)? + + + + + + open func updateCookies(force: Bool, retryCount: Int) { + addInvocation(.m_updateCookies__force_forceretryCount_retryCount(Parameter.value(`force`), Parameter.value(`retryCount`))) + let perform = methodPerformValue(.m_updateCookies__force_forceretryCount_retryCount(Parameter.value(`force`), Parameter.value(`retryCount`))) as? (Bool, Int) -> Void + perform?(`force`, `retryCount`) + } + + + fileprivate enum MethodType { + case m_updateCookies__force_forceretryCount_retryCount(Parameter, Parameter) + case p_authInteractor_get + case p_cookiesReady_get + case p_cookiesReady_set(Parameter) + case p_updatingCookies_get + case p_updatingCookies_set(Parameter) + case p_errorMessage_get + case p_errorMessage_set(Parameter) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_updateCookies__force_forceretryCount_retryCount(let lhsForce, let lhsRetrycount), .m_updateCookies__force_forceretryCount_retryCount(let rhsForce, let rhsRetrycount)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsForce, rhs: rhsForce, with: matcher), lhsForce, rhsForce, "force")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsRetrycount, rhs: rhsRetrycount, with: matcher), lhsRetrycount, rhsRetrycount, "retryCount")) + return Matcher.ComparisonResult(results) + case (.p_authInteractor_get,.p_authInteractor_get): return Matcher.ComparisonResult.match + case (.p_cookiesReady_get,.p_cookiesReady_get): return Matcher.ComparisonResult.match + case (.p_cookiesReady_set(let left),.p_cookiesReady_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_updatingCookies_get,.p_updatingCookies_get): return Matcher.ComparisonResult.match + case (.p_updatingCookies_set(let left),.p_updatingCookies_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_errorMessage_get,.p_errorMessage_get): return Matcher.ComparisonResult.match + case (.p_errorMessage_set(let left),.p_errorMessage_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + default: return .none + } + } + + func intValue() -> Int { + switch self { + case let .m_updateCookies__force_forceretryCount_retryCount(p0, p1): return p0.intValue + p1.intValue + case .p_authInteractor_get: return 0 + case .p_cookiesReady_get: return 0 + case .p_cookiesReady_set(let newValue): return newValue.intValue + case .p_updatingCookies_get: return 0 + case .p_updatingCookies_set(let newValue): return newValue.intValue + case .p_errorMessage_get: return 0 + case .p_errorMessage_set(let newValue): return newValue.intValue + } + } + func assertionName() -> String { + switch self { + case .m_updateCookies__force_forceretryCount_retryCount: return ".updateCookies(force:retryCount:)" + case .p_authInteractor_get: return "[get] .authInteractor" + case .p_cookiesReady_get: return "[get] .cookiesReady" + case .p_cookiesReady_set: return "[set] .cookiesReady" + case .p_updatingCookies_get: return "[get] .updatingCookies" + case .p_updatingCookies_set: return "[set] .updatingCookies" + case .p_errorMessage_get: return "[get] .errorMessage" + case .p_errorMessage_set: return "[set] .errorMessage" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + public static func authInteractor(getter defaultValue: AuthInteractorProtocol...) -> PropertyStub { + return Given(method: .p_authInteractor_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func cookiesReady(getter defaultValue: Bool...) -> PropertyStub { + return Given(method: .p_cookiesReady_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func updatingCookies(getter defaultValue: Bool...) -> PropertyStub { + return Given(method: .p_updatingCookies_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func errorMessage(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_errorMessage_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + + } + + public struct Verify { + fileprivate var method: MethodType + + public static func updateCookies(force: Parameter, retryCount: Parameter) -> Verify { return Verify(method: .m_updateCookies__force_forceretryCount_retryCount(`force`, `retryCount`))} + public static var authInteractor: Verify { return Verify(method: .p_authInteractor_get) } + public static var cookiesReady: Verify { return Verify(method: .p_cookiesReady_get) } + public static func cookiesReady(set newValue: Parameter) -> Verify { return Verify(method: .p_cookiesReady_set(newValue)) } + public static var updatingCookies: Verify { return Verify(method: .p_updatingCookies_get) } + public static func updatingCookies(set newValue: Parameter) -> Verify { return Verify(method: .p_updatingCookies_set(newValue)) } + public static var errorMessage: Verify { return Verify(method: .p_errorMessage_get) } + public static func errorMessage(set newValue: Parameter) -> Verify { return Verify(method: .p_errorMessage_set(newValue)) } + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + public static func updateCookies(force: Parameter, retryCount: Parameter, perform: @escaping (Bool, Int) -> Void) -> Perform { + return Perform(method: .m_updateCookies__force_forceretryCount_retryCount(`force`, `retryCount`), performs: perform) + } + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} +