├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── Pyrobase.podspec ├── Pyrobase.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ └── Pyrobase.xcscheme ├── Pyrobase ├── Info.plist ├── PlistReader.swift ├── PyroAuth.swift ├── PyroAuthInfo.plist ├── PyroAuthResult.swift ├── PyroEventSource.swift ├── PyroEventSourceMessage.swift ├── PyroEventSourceParser.swift ├── PyroEventSourceSessionProvider.swift ├── PyroTransaction.swift ├── PyroTransactionParameter.swift ├── PyroTransactionResult.swift ├── PyroTransactionTemporaryPath.swift ├── Pyrobase.h ├── Pyrobase.swift ├── Request.swift ├── RequestMethod.swift ├── RequestOperation.swift ├── RequestPath.swift ├── RequestResponse.swift └── RequestResult.swift ├── PyrobaseTests ├── AuthRequestMock.swift ├── CalendarExtensionTest.swift ├── Info.plist ├── JSONRequestOperationTest.swift ├── JSONSerializationMock.swift ├── PlistReaderContentMock.swift ├── PlistReaderResourceMock.swift ├── PlistReaderSample.plist ├── PlistReaderTest.swift ├── PlistSerializationMock.swift ├── PyroAuthContentTest.swift ├── PyroAuthMock.swift ├── PyroAuthTest.swift ├── PyroAuthTokenContentTest.swift ├── PyroEventSourceMessageTest.swift ├── PyroEventSourceParserTest.swift ├── PyroEventSourceSessionProviderMock.swift ├── PyroEventSourceSessionProviderTest.swift ├── PyroEventSourceTest.swift ├── PyroTransactionElapsedTimeMock.swift ├── PyroTransactionMock.swift ├── PyroTransactionTemporaryPathMock.swift ├── PyroTransactionTemporaryPathTest.swift ├── PyroTransactionTest.swift ├── PyrobaseTest.swift ├── RequestErrorTest.swift ├── RequestMethodTest.swift ├── RequestMock.swift ├── RequestOperationMock.swift ├── RequestPathTest.swift ├── RequestResponseTest.swift ├── RequestTest.swift ├── URLSessionDataDelegateMock.swift └── URLSessionMock.swift └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # From https://github.com/github/gitignore/blob/master/Swift.gitignore 2 | 3 | GoogleService-Info.plist 4 | *.gpx 5 | 6 | # Xcode 7 | # 8 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 9 | 10 | # macOS 11 | .DS_Store 12 | 13 | ## Build generated 14 | build/ 15 | DerivedData/ 16 | 17 | ## Various settings 18 | *.pbxuser 19 | !default.pbxuser 20 | *.mode1v3 21 | !default.mode1v3 22 | *.mode2v3 23 | !default.mode2v3 24 | *.perspectivev3 25 | !default.perspectivev3 26 | xcuserdata/ 27 | 28 | ## Other 29 | *.moved-aside 30 | *.xcuserstate 31 | 32 | ## Obj-C/Swift specific 33 | *.hmap 34 | *.ipa 35 | *.dSYM.zip 36 | *.dSYM 37 | 38 | ## Playgrounds 39 | timeline.xctimeline 40 | playground.xcworkspace 41 | *.playground 42 | 43 | # Swift Package Manager 44 | # 45 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 46 | # Packages/ 47 | .build/ 48 | 49 | # CocoaPods 50 | # 51 | # We recommend against adding the Pods directory to your .gitignore. However 52 | # you should judge for yourself, the pros and cons are mentioned at: 53 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 54 | 55 | Pods/ 56 | 57 | # Carthage 58 | # 59 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 60 | 61 | Carthage/Checkouts 62 | Carthage/Build 63 | 64 | # fastlane 65 | # 66 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 67 | # screenshots whenever they are needed. 68 | # For more information about the recommended setup visit: 69 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 70 | 71 | fastlane/report.xml 72 | fastlane/Preview.html 73 | fastlane/screenshots 74 | fastlane/test_output 75 | 76 | 77 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Build 1.0.1 4 | - Improved usability of errors 5 | - Added code and message property in `RequestError` 6 | - `RequestError` implemented `Equatable` protocol 7 | - `RequestError` is equal if code and message are the same 8 | - Defined static message for `invalidURL`, `unparseableJSON`, `noURLResponse`, `nullJSON`, and `unknown` cases 9 | - Defined code for `RequestError` cases 10 | 11 | ## Version 1.0 12 | - Implemented simple Firebase REST API 13 | - User can do authentication 14 | - User can issue GET, POST, PUT, PATCH requests 15 | - User can stream paths 16 | - User can run transactions 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 mownier 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Pyrobase.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'Pyrobase' 3 | s.version = '1.1' 4 | s.summary = 'An iOS lightweight wrapper for Firebase REST API' 5 | s.platform = :ios, '9.0' 6 | s.license = { :type => 'MIT', :file => 'LICENSE' } 7 | s.homepage = 'https://github.com/mownier/pyrobase' 8 | s.author = { 'Mounir Ybanez' => 'rinuom91@gmail.com' } 9 | s.source = { :git => 'https://github.com/mownier/pyrobase.git', :tag => s.version.to_s } 10 | s.source_files = 'Pyrobase/*.swift' 11 | s.requires_arc = true 12 | end 13 | -------------------------------------------------------------------------------- /Pyrobase.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Pyrobase.xcodeproj/xcshareddata/xcschemes/Pyrobase.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | 35 | 41 | 42 | 43 | 45 | 46 | 47 | 48 | 49 | 50 | 56 | 57 | 58 | 59 | 60 | 61 | 72 | 73 | 79 | 80 | 81 | 82 | 86 | 87 | 88 | 89 | 90 | 91 | 97 | 98 | 104 | 105 | 106 | 107 | 109 | 110 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /Pyrobase/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.1.0 19 | CFBundleVersion 20 | 1.1.0 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Pyrobase/PlistReader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlistReader.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 09/06/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | protocol PlistReaderContentProtocol { 10 | 11 | func extract(at path: String) -> Data? 12 | } 13 | 14 | protocol PlistReaderResourceProtocol { 15 | 16 | func path(for name: String) -> String? 17 | } 18 | 19 | enum PlistReaderError: Error { 20 | 21 | case notFound 22 | case noContent 23 | case unreadable 24 | } 25 | 26 | class PlistReader { 27 | 28 | internal(set) var data: Any! 29 | internal(set) var name: String 30 | 31 | init(name: String, resource: PlistReaderResourceProtocol, content: PlistReaderContentProtocol, format: PropertyListSerialization.PropertyListFormat, serialization: PropertyListSerialization.Type) throws { 32 | self.name = name 33 | 34 | guard let path = resource.path(for: name) else { 35 | throw PlistReaderError.notFound 36 | } 37 | 38 | guard let xml = content.extract(at: path) else { 39 | throw PlistReaderError.noContent 40 | } 41 | 42 | var plistFormat = format 43 | 44 | guard let data = try? serialization.propertyList(from: xml, options: [], format: &plistFormat) else { 45 | throw PlistReaderError.unreadable 46 | } 47 | 48 | self.data = data 49 | } 50 | } 51 | 52 | extension PlistReader { 53 | 54 | class func create(name: String, bundle: Bundle = .main, fileManager: FileManager = .default) -> PlistReader? { 55 | let format = PropertyListSerialization.PropertyListFormat.xml 56 | let serialization = PropertyListSerialization.self 57 | let reader = try? PlistReader(name: name, resource: bundle, content: fileManager, format: format, serialization: serialization) 58 | return reader 59 | } 60 | } 61 | 62 | extension Bundle: PlistReaderResourceProtocol { 63 | 64 | func path(for name: String) -> String? { 65 | return path(forResource: name, ofType: "plist") 66 | } 67 | } 68 | 69 | extension FileManager: PlistReaderContentProtocol { 70 | 71 | func extract(at path: String) -> Data? { 72 | return contents(atPath: path) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Pyrobase/PyroAuth.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PyroAuth.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 05/06/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | public class PyroAuth { 10 | 11 | internal var key: String 12 | internal var request: RequestProtocol 13 | internal var signInPath: String 14 | internal var registerPath: String 15 | internal var refreshPath: String 16 | internal var confirmationCodePath: String 17 | 18 | public class func create(key: String, bundleIdentifier: String = "com.ner.Pyrobase", plistName: String = "PyroAuthInfo", request: RequestProtocol = Request.create() ) -> PyroAuth? { 19 | guard let bundle = Bundle(identifier: bundleIdentifier) else { 20 | return nil 21 | } 22 | 23 | guard let reader = PlistReader.create(name: plistName, bundle: bundle) else { 24 | return nil 25 | } 26 | 27 | var registerPath: String = "" 28 | var signInPath: String = "" 29 | var refreshPath: String = "" 30 | var confirmationCodePath: String = "" 31 | 32 | if let readerInfo = reader.data as? [AnyHashable: Any] { 33 | registerPath = (readerInfo["register_path"] as? String) ?? "" 34 | signInPath = (readerInfo["sign_in_path"] as? String) ?? "" 35 | refreshPath = (readerInfo["refresh_path"] as? String) ?? "" 36 | confirmationCodePath = (readerInfo["confirmation_code_path"] as? String) ?? "" 37 | } 38 | 39 | let auth = PyroAuth(key: key, request: request, signInPath: signInPath, registerPath: registerPath, refreshPath: refreshPath, confirmationCodePath: confirmationCodePath) 40 | return auth 41 | } 42 | 43 | public init(key: String, request: RequestProtocol, signInPath: String, registerPath: String, refreshPath: String, confirmationCodePath: String) { 44 | self.key = key 45 | self.request = request 46 | self.signInPath = signInPath 47 | self.registerPath = registerPath 48 | self.refreshPath = refreshPath 49 | self.confirmationCodePath = confirmationCodePath 50 | } 51 | 52 | public func register(email: String, password: String, completion: @escaping (PyroAuthResult) -> Void) { 53 | let data: [AnyHashable: Any] = ["email": email, "password": password, "returnSecureToken": true] 54 | let path = "\(registerPath)?key=\(key)" 55 | request.write(path: path, method: .post, data: data) { result in 56 | self.handleRequestResult(result, completion: completion) 57 | } 58 | } 59 | 60 | public func signIn(email: String, password: String, completion: @escaping (PyroAuthResult) -> Void) { 61 | let data: [AnyHashable: Any] = ["email": email, "password": password, "returnSecureToken": true] 62 | let path = "\(signInPath)?key=\(key)" 63 | request.write(path: path, method: .post, data: data) { result in 64 | self.handleRequestResult(result, completion: completion) 65 | } 66 | } 67 | 68 | public func refresh(token: String, completion: @escaping (PyroAuthResult) -> Void) { 69 | let data: [AnyHashable: Any] = ["grant_type": "refresh_token", "refresh_token": token] 70 | let path = "\(refreshPath)?key=\(key)" 71 | request.write(path: path, method: .post, data: data) { result in 72 | switch result { 73 | case .succeeded(let info): 74 | guard let resultInfo = info as? [AnyHashable: Any] else { 75 | completion(.failed(PyroAuthError.unexpectedContent)) 76 | return 77 | } 78 | 79 | guard let accessToken = resultInfo["access_token"] as? String, 80 | let refreshToken = resultInfo["refresh_token"] as? String, 81 | let expiration = resultInfo["expires_in"] as? String else { 82 | return completion(.failed(PyroAuthError.incompleteContent)) 83 | } 84 | 85 | var content = PyroAuthTokenContent() 86 | content.accessToken = accessToken 87 | content.refreshToken = refreshToken 88 | content.expiration = expiration 89 | 90 | completion(.succeeded(content)) 91 | 92 | case .failed(let info): 93 | completion(.failed(info)) 94 | } 95 | } 96 | } 97 | 98 | public func sendPasswordReset(email: String, completion: @escaping (PyroAuthResult) -> Void) { 99 | let data: [AnyHashable: Any] = ["requestType": "PASSWORD_RESET", "email": email] 100 | let path = "\(confirmationCodePath)?key=\(key)" 101 | request.write(path: path, method: .post, data: data) { result in 102 | switch result { 103 | case .succeeded: 104 | completion(.succeeded(true)) 105 | 106 | case .failed(let info): 107 | completion(.failed(info)) 108 | } 109 | } 110 | } 111 | 112 | internal func handleRequestResult(_ result: RequestResult, completion: @escaping (PyroAuthResult) -> Void) { 113 | switch result { 114 | case .succeeded(let info): 115 | guard let resultInfo = info as? [AnyHashable: Any] else { 116 | completion(.failed(PyroAuthError.unexpectedContent)) 117 | return 118 | } 119 | 120 | guard let userId = resultInfo["localId"] as? String, 121 | let email = resultInfo["email"] as? String, 122 | let accessToken = resultInfo["idToken"] as? String, 123 | let refreshToken = resultInfo["refreshToken"] as? String, 124 | let expiration = resultInfo["expiresIn"] as? String else { 125 | return completion(.failed(PyroAuthError.incompleteContent)) 126 | } 127 | 128 | var content = PyroAuthContent() 129 | content.userId = userId 130 | content.email = email 131 | content.accessToken = accessToken 132 | content.refreshToken = refreshToken 133 | content.expiration = expiration 134 | 135 | completion(.succeeded(content)) 136 | 137 | case .failed(let info): 138 | completion(.failed(info)) 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /Pyrobase/PyroAuthInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | confirmation_code_path 6 | https://www.googleapis.com/identitytoolkit/v3/relyingparty/getOobConfirmationCode 7 | refresh_path 8 | https://securetoken.googleapis.com/v1/token 9 | register_path 10 | https://www.googleapis.com/identitytoolkit/v3/relyingparty/signupNewUser 11 | sign_in_path 12 | https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword 13 | 14 | 15 | -------------------------------------------------------------------------------- /Pyrobase/PyroAuthResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PyroAuthResult.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 10/06/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | public enum PyroAuthResult { 10 | 11 | case failed(Error) 12 | case succeeded(T) 13 | } 14 | 15 | public enum PyroAuthError: Error { 16 | 17 | case unexpectedContent 18 | case incompleteContent 19 | } 20 | 21 | public struct PyroAuthContent { 22 | 23 | internal(set) public var userId: String 24 | internal(set) public var accessToken: String 25 | internal(set) public var email: String 26 | internal(set) public var refreshToken: String 27 | internal(set) public var expiration: String 28 | 29 | public init() { 30 | self.userId = "" 31 | self.accessToken = "" 32 | self.email = "" 33 | self.refreshToken = "" 34 | self.expiration = "" 35 | } 36 | } 37 | 38 | public struct PyroAuthTokenContent { 39 | 40 | internal(set) public var accessToken: String 41 | internal(set) public var expiration: String 42 | internal(set) public var refreshToken: String 43 | 44 | public init() { 45 | self.accessToken = "" 46 | self.expiration = "" 47 | self.refreshToken = "" 48 | } 49 | } 50 | 51 | extension PyroAuthContent: CustomStringConvertible { 52 | 53 | public var description: String { 54 | return "userId: \(userId)\naccessToken: \(accessToken)\nemail: \(email)\nrefreshToken: \(refreshToken)\nexpiration: \(expiration)" 55 | } 56 | } 57 | 58 | extension PyroAuthTokenContent: CustomStringConvertible { 59 | 60 | public var description: String { 61 | return "accessToken: \(accessToken)\nexpiration: \(expiration)\nrefreshToken: \(refreshToken)" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Pyrobase/PyroEventSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PyroEventSource.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 20/06/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | public enum PyroEventSourceError: Error { 10 | 11 | case notClosed 12 | case notOpen 13 | case forcedClose 14 | } 15 | 16 | public enum PyroEventSourceState { 17 | 18 | case connecting 19 | case open 20 | case closed 21 | } 22 | 23 | public protocol PyroEventSourceCallback: class { 24 | 25 | func pyroEventSourceOnConnecting(_ eventSource: PyroEventSource) 26 | func pyroEventSourceOnOpen(_ eventSource: PyroEventSource) 27 | func pyroEventSourceOnClosed(_ eventSource: PyroEventSource) 28 | func pyroEventSource(_ eventSource: PyroEventSource, didReceiveMessage message: PyroEventSourceMessage) 29 | func pyroEventSource(_ eventSource: PyroEventSource, didReceiveError error: Error) 30 | } 31 | 32 | public class PyroEventSource: NSObject { 33 | 34 | internal var path: RequestPathProtocol 35 | internal var response: RequestResponseProtocol 36 | internal var sessionProvider: PyroEventSourceSessionProviderProtocol 37 | internal var parser: PyroEventSourceParserProtocol 38 | internal var session: URLSession! 39 | internal(set) public var lastEventID: String 40 | internal(set) public var state: PyroEventSourceState { 41 | didSet { 42 | switch state { 43 | case .open: callback?.pyroEventSourceOnOpen(self) 44 | case .closed: callback?.pyroEventSourceOnClosed(self) 45 | case .connecting: callback?.pyroEventSourceOnConnecting(self) 46 | } 47 | } 48 | } 49 | 50 | weak public var callback: PyroEventSourceCallback? 51 | 52 | public class func create(baseURL: String, accessToken: String, lastEventID: String = "") -> PyroEventSource { 53 | let provider = PyroEventSourceSessionProvider.create() 54 | let path = RequestPath(baseURL: baseURL, accessToken: accessToken) 55 | let parser = PyroEventSourceParser() 56 | let response = RequestResponse() 57 | let eventSource = PyroEventSource(path: path, response: response, sessionProvider: provider, parser: parser, lastEventID: lastEventID) 58 | return eventSource 59 | } 60 | 61 | public init(path: RequestPathProtocol, response: RequestResponseProtocol, sessionProvider: PyroEventSourceSessionProvider, parser: PyroEventSourceParserProtocol, lastEventID: String) { 62 | self.path = path 63 | self.response = response 64 | self.sessionProvider = sessionProvider 65 | self.lastEventID = lastEventID 66 | self.state = .closed 67 | self.parser = parser 68 | } 69 | 70 | public func close() { 71 | session?.invalidateAndCancel() 72 | session = nil 73 | state = .closed 74 | } 75 | 76 | public func stream(_ relativePath: String) { 77 | guard state == .closed else { 78 | callback?.pyroEventSource(self, didReceiveError: PyroEventSourceError.notClosed) 79 | return 80 | } 81 | 82 | guard let url = URL(string: path.build(relativePath)) else { 83 | callback?.pyroEventSource(self, didReceiveError: RequestError.invalidURL) 84 | return 85 | } 86 | 87 | session = sessionProvider.createSession(for: self, lastEventID: lastEventID) 88 | state = .connecting 89 | session.dataTask(with: url).resume() 90 | } 91 | 92 | internal func isForcedClose(_ response: HTTPURLResponse?) -> Bool { 93 | return response != nil && response!.statusCode == 204 94 | } 95 | } 96 | 97 | extension PyroEventSource: URLSessionDataDelegate { 98 | 99 | public func urlSession(_ session: URLSession, dataTask task: URLSessionDataTask, didReceive data: Data) { 100 | guard state == .open else { 101 | callback?.pyroEventSource(self, didReceiveError: PyroEventSourceError.notOpen) 102 | return 103 | } 104 | 105 | guard !isForcedClose(task.response as? HTTPURLResponse) else { 106 | close() 107 | callback?.pyroEventSource(self, didReceiveError: PyroEventSourceError.forcedClose) 108 | return 109 | } 110 | 111 | if let responseError = response.isErroneous(task.response as? HTTPURLResponse, data: data) { 112 | close() 113 | callback?.pyroEventSource(self, didReceiveError: responseError) 114 | 115 | } else { 116 | let message = parser.parse(data) 117 | callback?.pyroEventSource(self, didReceiveMessage: message) 118 | lastEventID = message.id 119 | } 120 | } 121 | 122 | public func urlSession(_ session: URLSession, dataTask task: URLSessionDataTask, didReceive httpResponse: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { 123 | if let responseError = response.isErroneous(httpResponse as? HTTPURLResponse, data: nil) { 124 | close() 125 | callback?.pyroEventSource(self, didReceiveError: responseError) 126 | 127 | } else { 128 | state = .open 129 | completionHandler(.allow) 130 | } 131 | } 132 | 133 | public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { 134 | close() 135 | 136 | guard !isForcedClose(task.response as? HTTPURLResponse) else { 137 | callback?.pyroEventSource(self, didReceiveError: PyroEventSourceError.forcedClose) 138 | return 139 | } 140 | 141 | if let responseError = response.isErroneous(task.response as? HTTPURLResponse, data: nil) { 142 | callback?.pyroEventSource(self, didReceiveError: responseError) 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /Pyrobase/PyroEventSourceMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PyroEventSourceMessage.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 22/06/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | public struct PyroEventSourceMessage { 10 | 11 | public var id: String 12 | public var event: String 13 | public var data: String 14 | 15 | public init() { 16 | self.id = "" 17 | self.event = "" 18 | self.data = "" 19 | } 20 | } 21 | 22 | extension PyroEventSourceMessage: CustomStringConvertible { 23 | 24 | public var description: String { 25 | return "\nid: \(id)\nevent: \(event)\ndata: \(data)\n" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Pyrobase/PyroEventSourceParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PyroEventSourceParser.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 22/06/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | public protocol PyroEventSourceParserProtocol { 10 | 11 | func parse(_ data: Data) -> PyroEventSourceMessage 12 | } 13 | 14 | public class PyroEventSourceParser: PyroEventSourceParserProtocol { 15 | 16 | public func parse(_ data: Data) -> PyroEventSourceMessage { 17 | var message = PyroEventSourceMessage() 18 | 19 | guard let string = String(data: data, encoding: .utf8) else { 20 | return message 21 | } 22 | 23 | for event in string.components(separatedBy: CharacterSet.newlines) { 24 | guard !event.isEmpty && !event.hasPrefix(":") && !event.contains("retry:") else { 25 | continue 26 | } 27 | 28 | for line in event.components(separatedBy: CharacterSet.newlines) { 29 | guard let colonRange = line.range(of: ":") else { 30 | continue 31 | } 32 | 33 | let keyRange = Range(uncheckedBounds: (line.startIndex, colonRange.lowerBound)) 34 | let key = String(line[keyRange]).trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) 35 | 36 | let valueRange = Range(uncheckedBounds: (line.index(after: colonRange.lowerBound), line.endIndex)) 37 | let value = String(line[valueRange]).trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) 38 | 39 | switch key.lowercased() { 40 | case "id": 41 | message.id = value 42 | 43 | case "event": 44 | message.event = value 45 | 46 | case "data": 47 | message.data = value 48 | 49 | default: break 50 | } 51 | } 52 | } 53 | 54 | return message 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Pyrobase/PyroEventSourceSessionProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PyroEventSourceSessionProvider.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 22/06/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | public protocol PyroEventSourceSessionProviderProtocol { 10 | 11 | var queue: OperationQueue { get } 12 | var headers: [String: String] { set get } 13 | 14 | func createSession(for delegate: URLSessionDataDelegate, lastEventID: String) -> URLSession 15 | } 16 | 17 | public class PyroEventSourceSessionProvider: PyroEventSourceSessionProviderProtocol { 18 | 19 | public var queue: OperationQueue 20 | public var headers: [String: String] 21 | 22 | public class func create() -> PyroEventSourceSessionProvider { 23 | let queue = OperationQueue() 24 | let headers = [ 25 | "Accept": "text/event-stream" 26 | ] 27 | return PyroEventSourceSessionProvider(queue: queue, headers: headers) 28 | } 29 | 30 | public init(queue: OperationQueue, headers: [String: String]) { 31 | self.queue = queue 32 | self.headers = headers 33 | } 34 | 35 | public func createSession(for delegate: URLSessionDataDelegate, lastEventID: String) -> URLSession { 36 | let config = URLSessionConfiguration.default 37 | config.timeoutIntervalForRequest = TimeInterval(INT_MAX) 38 | config.timeoutIntervalForResource = TimeInterval(INT_MAX) 39 | config.httpAdditionalHeaders = headers 40 | if !lastEventID.isEmpty { 41 | config.httpAdditionalHeaders!["Last-Event-Id"] = lastEventID 42 | } 43 | return URLSession(configuration: config, delegate: delegate, delegateQueue: queue) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Pyrobase/PyroTransaction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PyroTransaction.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 15/06/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | public class PyroTransaction { 10 | 11 | internal var path: RequestPathProtocol 12 | internal var request: RequestProtocol 13 | internal var tempPath: PyroTransactionTemporaryPathProtocol 14 | 15 | public var baseURL: String { 16 | return path.baseURL 17 | } 18 | 19 | public class func create(baseURL: String, accessToken: String) -> PyroTransaction { 20 | let path = RequestPath(baseURL: baseURL, accessToken: accessToken) 21 | let request = Request.create() 22 | let tempPath = PyroTransactionTemporaryPath.create() 23 | let transaction = PyroTransaction(request: request, path: path, tempPath: tempPath) 24 | return transaction 25 | } 26 | 27 | public init(request: RequestProtocol, path: RequestPathProtocol, tempPath: PyroTransactionTemporaryPathProtocol) { 28 | self.request = request 29 | self.path = path 30 | self.tempPath = tempPath 31 | } 32 | 33 | public func run(parentPath: String, childKey: String, mutator: @escaping (Any) -> Any, completion: @escaping (PyroTransactionResult) -> Void) { 34 | let param = Parameter(parentPath: parentPath, childKey: childKey, mutator: mutator, completion: completion) 35 | 36 | readTransaction(param: param) 37 | } 38 | 39 | internal func readTransaction(param: Parameter) { 40 | let readPath = path.build("\(tempPath.key)/\(param.parentPath)/\(param.childKey)") 41 | 42 | request.read(path: readPath, query: [:]) { result in 43 | switch result { 44 | case .failed(let info): 45 | guard let errorInfo = info as? RequestError, errorInfo == .nullJSON else { 46 | param.completion(.failed(info)) 47 | return 48 | } 49 | 50 | self.writeTransaction(param: param) 51 | 52 | case .succeeded(let info): 53 | guard let string = info as? String, let timestamp = Double(string) else { 54 | param.completion(.failed(PyroTransactionError.invalidExpirationTimestamp)) 55 | return 56 | } 57 | 58 | if self.tempPath.isTransactionDateExpired(timestamp, now: Date()) { 59 | self.writeTransaction(param: param) 60 | 61 | } else { 62 | param.completion(.failed(PyroTransactionError.activeTransactionNotDone)) 63 | } 64 | } 65 | } 66 | } 67 | 68 | internal func writeTransaction(param: Parameter) { 69 | let info = ["\(param.parentPath)/\(param.childKey)": [".sv": "timestamp"]] 70 | let writePath = path.build(tempPath.key) 71 | 72 | request.write(path: writePath, method: .patch, data: info) { result in 73 | switch result { 74 | case .failed(let info): 75 | param.completion(.failed(info)) 76 | 77 | case .succeeded: 78 | self.readChild(param: param) 79 | } 80 | } 81 | } 82 | 83 | internal func readChild(param: Parameter) { 84 | let readPath = path.build("\(param.parentPath)/\(param.childKey)") 85 | 86 | request.read(path: readPath, query: [:]) { result in 87 | switch result { 88 | case .failed(let info): 89 | self.deleteTransaction(param: param) { _ in 90 | param.completion(.failed(info)) 91 | } 92 | 93 | case .succeeded(let info): 94 | self.writeChild(param: param, info: info) 95 | } 96 | } 97 | } 98 | 99 | internal func writeChild(param: Parameter, info: Any) { 100 | let newInfo = param.mutator(info) 101 | let writePath = path.build(param.parentPath) 102 | let data = [param.childKey: newInfo] 103 | 104 | request.write(path: writePath, method: .patch, data: data) { result in 105 | let completion: (RequestResult) -> Void 106 | 107 | switch result { 108 | case .failed(let info): 109 | completion = { _ in param.completion(.failed(info)) } 110 | 111 | case .succeeded(let info): 112 | completion = { _ in param.completion(.succeeded(info)) } 113 | } 114 | 115 | self.deleteTransaction(param: param, completion: completion) 116 | } 117 | } 118 | 119 | internal func deleteTransaction(param: Parameter, completion: @escaping (RequestResult) -> Void) { 120 | let deletePath = path.build("\(tempPath.key)/\(param.parentPath)") 121 | 122 | request.delete(path: deletePath) { result in 123 | completion(result) 124 | } 125 | } 126 | } 127 | 128 | -------------------------------------------------------------------------------- /Pyrobase/PyroTransactionParameter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PyroTransactionParameter.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 16/06/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | struct Parameter { 10 | 11 | let parentPath: String 12 | let childKey: String 13 | let mutator: (Any) -> Any 14 | let completion: (PyroTransactionResult) -> Void 15 | 16 | init(parentPath: String, childKey: String, mutator: @escaping (Any) -> Any, completion: @escaping (PyroTransactionResult) -> Void) { 17 | self.parentPath = parentPath 18 | self.childKey = childKey 19 | self.mutator = mutator 20 | self.completion = completion 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Pyrobase/PyroTransactionResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PyroTransactionResult.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 19/06/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | public enum PyroTransactionResult { 10 | 11 | case failed(Error) 12 | case succeeded(Any) 13 | } 14 | 15 | public enum PyroTransactionError: Error { 16 | 17 | case invalidExpirationTimestamp 18 | case activeTransactionNotDone 19 | } 20 | -------------------------------------------------------------------------------- /Pyrobase/PyroTransactionTemporaryPath.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PyroTransactionTemporaryPath.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 19/06/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | public protocol PyroTransactionTemporaryPathProtocol { 10 | 11 | var key: String { get } 12 | var expiration: UInt { get } 13 | 14 | func isTransactionDateExpired(_ timestamp: Double, now: Date) -> Bool 15 | } 16 | 17 | public class PyroTransactionTemporaryPath: PyroTransactionTemporaryPathProtocol { 18 | 19 | internal var elapsedTime: PyroTransactionElapsedTimeProtocol 20 | 21 | internal(set) public var key: String 22 | internal(set) public var expiration: UInt 23 | 24 | public class func create() -> PyroTransactionTemporaryPath { 25 | let key: String = "pyrobase_transactions" 26 | let expiration: UInt = 30 27 | let elapsedTime: Calendar = .current 28 | return PyroTransactionTemporaryPath(key: key, expiration: expiration, elapsedTime: elapsedTime) 29 | } 30 | 31 | public init(key: String, expiration: UInt, elapsedTime: PyroTransactionElapsedTimeProtocol) { 32 | self.key = key 33 | self.expiration = expiration 34 | self.elapsedTime = elapsedTime 35 | } 36 | 37 | public func isTransactionDateExpired(_ timestamp: Double, now: Date) -> Bool { 38 | let transactionDate = Date(timeIntervalSince1970: timestamp / 1000) 39 | 40 | guard let seconds: Int = elapsedTime.seconds(from: transactionDate, to: now) else { 41 | return false 42 | } 43 | 44 | return seconds >= Int(expiration) 45 | } 46 | } 47 | 48 | public protocol PyroTransactionElapsedTimeProtocol { 49 | 50 | func seconds(from: Date, to: Date) -> Int? 51 | } 52 | 53 | extension Calendar: PyroTransactionElapsedTimeProtocol { 54 | 55 | public func seconds(from transactionDate: Date, to now: Date) -> Int? { 56 | return dateComponents([.second], from: now, to: transactionDate).second 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Pyrobase/Pyrobase.h: -------------------------------------------------------------------------------- 1 | // 2 | // Pyrobase.h 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 01/05/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for Pyrobase. 12 | FOUNDATION_EXPORT double PyrobaseVersionNumber; 13 | 14 | //! Project version string for Pyrobase. 15 | FOUNDATION_EXPORT const unsigned char PyrobaseVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Pyrobase/Pyrobase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Pyrobase.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 01/05/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | public class Pyrobase { 10 | 11 | internal var path: RequestPathProtocol 12 | internal var request: RequestProtocol 13 | 14 | public var baseURL: String { 15 | return path.baseURL 16 | } 17 | 18 | public init(request: RequestProtocol, path: RequestPathProtocol) { 19 | self.request = request 20 | self.path = path 21 | } 22 | 23 | public func get(path relativePath: String, query: [AnyHashable: Any], completion: @escaping (RequestResult) -> Void) { 24 | request.read(path: path.build(relativePath), query: query) { result in 25 | completion(result) 26 | } 27 | } 28 | 29 | public func put(path relativePath: String, value: [AnyHashable: Any], completion: @escaping (RequestResult) -> Void) { 30 | request.write(path: path.build(relativePath), method: .put, data: value) { result in 31 | completion(result) 32 | } 33 | } 34 | 35 | public func post(path relativePath: String, value: [AnyHashable: Any], completion: @escaping (RequestResult) -> Void) { 36 | request.write(path: path.build(relativePath), method: .post, data: value) { result in 37 | completion(result) 38 | } 39 | } 40 | 41 | public func patch(path relativePath: String, value: [AnyHashable: Any], completion: @escaping (RequestResult) -> Void) { 42 | request.write(path: path.build(relativePath), method: .patch, data: value) { result in 43 | completion(result) 44 | } 45 | } 46 | 47 | public func delete(path relativePath: String, completion: @escaping (RequestResult) -> Void) { 48 | request.delete(path: path.build(relativePath), completion: completion) 49 | } 50 | } 51 | 52 | extension Pyrobase { 53 | 54 | public class func create(baseURL: String, accessToken: String) -> Pyrobase { 55 | let path = RequestPath(baseURL: baseURL, accessToken: accessToken) 56 | let request = Request.create() 57 | let pyrobase = Pyrobase(request: request, path: path) 58 | return pyrobase 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Pyrobase/Request.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Request.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 02/05/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | public protocol RequestProtocol { 10 | 11 | func read(path: String, query: [AnyHashable: Any], completion: @escaping (RequestResult) -> Void) 12 | func write(path: String, method: RequestMethod, data: [AnyHashable: Any], completion: @escaping (RequestResult) -> Void) 13 | func delete(path: String, completion: @escaping (RequestResult) -> Void) 14 | } 15 | 16 | public class Request: RequestProtocol { 17 | 18 | internal var session: URLSession 19 | internal var operation: RequestOperation 20 | internal var response: RequestResponseProtocol 21 | 22 | public init(session: URLSession, operation: RequestOperation, response: RequestResponseProtocol) { 23 | self.session = session 24 | self.operation = operation 25 | self.response = response 26 | } 27 | 28 | public func read(path: String, query: [AnyHashable: Any], completion: @escaping (RequestResult) -> Void) { 29 | request(path, .get, query, completion) 30 | } 31 | 32 | public func write(path: String, method: RequestMethod, data: [AnyHashable: Any], completion: @escaping (RequestResult) -> Void) { 33 | request(path, method, data, completion) 34 | } 35 | 36 | public func delete(path: String, completion: @escaping (RequestResult) -> Void) { 37 | request(path, .delete, [:], completion) 38 | } 39 | 40 | internal func request(_ path: String, _ method: RequestMethod, _ data: [AnyHashable: Any], _ completion: @escaping (RequestResult) -> Void) { 41 | guard let url = buildURL(path, method, data) else { 42 | completion(.failed(RequestError.invalidURL)) 43 | return 44 | } 45 | 46 | let request = operation.build(url: url, method: method, data: data) 47 | let task = session.dataTask(with: request) { data, response, error in 48 | guard error == nil else { 49 | completion(.failed(error!)) 50 | return 51 | } 52 | 53 | guard response != nil else { 54 | completion(.failed(RequestError.noURLResponse)) 55 | return 56 | } 57 | 58 | let error = self.response.isErroneous(response as? HTTPURLResponse, data: data) 59 | 60 | guard error == nil else { 61 | completion(.failed(error!)) 62 | return 63 | } 64 | 65 | guard data != nil else { 66 | completion(.succeeded([:])) 67 | return 68 | } 69 | 70 | let result = self.operation.parse(data: data!) 71 | 72 | switch result { 73 | case .error(let info): 74 | completion(.failed(info)) 75 | 76 | case .okay(let info): 77 | guard let okayInfo = info as? String, okayInfo.lowercased() == "null", method != .delete else { 78 | completion(.succeeded(info)) 79 | return 80 | } 81 | 82 | completion(.failed(RequestError.nullJSON)) 83 | } 84 | } 85 | task.resume() 86 | } 87 | 88 | internal func buildURL(_ path: String, _ method: RequestMethod, _ data: [AnyHashable: Any]) -> URL? { 89 | switch method { 90 | case .get where !data.isEmpty: 91 | guard !path.isEmpty, var components = URLComponents(string: path) else { 92 | return nil 93 | } 94 | 95 | var queryItems = [URLQueryItem]() 96 | 97 | for (key, value) in data { 98 | let item = URLQueryItem(name: "\(key)", value: "\(value)") 99 | queryItems.insert(item, at: 0) 100 | } 101 | 102 | if components.queryItems != nil { 103 | components.queryItems!.append(contentsOf: queryItems) 104 | 105 | } else { 106 | components.queryItems = queryItems 107 | } 108 | 109 | return components.url 110 | 111 | default: 112 | return URL(string: path) 113 | } 114 | } 115 | } 116 | 117 | extension Request { 118 | 119 | public class func create() -> Request { 120 | let session = URLSession.shared 121 | let operation = JSONRequestOperation.create() 122 | let response = RequestResponse() 123 | let request = Request(session: session, operation: operation, response: response) 124 | return request 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Pyrobase/RequestMethod.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestMethod.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 05/06/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | public enum RequestMethod { 10 | 11 | case get 12 | case put 13 | case patch 14 | case post 15 | case delete 16 | } 17 | 18 | extension RequestMethod: CustomStringConvertible { 19 | 20 | public var description: String { 21 | switch self { 22 | case .get: return "GET" 23 | case .put: return "PUT" 24 | case .patch: return "PATCH" 25 | case .post: return "POST" 26 | case .delete: return "DELETE" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Pyrobase/RequestOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestOperation.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 05/06/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | public protocol RequestOperation { 10 | 11 | func build(url: URL, method: RequestMethod, data: [AnyHashable: Any]) -> URLRequest 12 | func parse(data: Data) -> RequestOperationResult 13 | } 14 | 15 | public enum RequestOperationResult { 16 | 17 | case error(Error) 18 | case okay(Any) 19 | } 20 | 21 | public class JSONRequestOperation: RequestOperation { 22 | 23 | internal var serialization: JSONSerialization.Type 24 | 25 | public init(serialization: JSONSerialization.Type) { 26 | self.serialization = serialization 27 | } 28 | 29 | public func build(url: URL, method: RequestMethod, data: [AnyHashable: Any]) -> URLRequest { 30 | var request = URLRequest(url: url) 31 | request.addValue("application/json", forHTTPHeaderField: "Accept") 32 | request.addValue("application/json", forHTTPHeaderField: "Content-Type") 33 | request.httpMethod = "\(method)" 34 | 35 | if !data.isEmpty && method != .get { 36 | request.httpBody = try? serialization.data(withJSONObject: data, options: []) 37 | } 38 | 39 | return request 40 | } 41 | 42 | public func parse(data: Data) -> RequestOperationResult { 43 | if serialization.isValidJSONObject(data) { 44 | guard let jsonObject = try? serialization.jsonObject(with: data, options: []) else { 45 | return .error(RequestError.unparseableJSON) 46 | } 47 | 48 | return .okay(jsonObject) 49 | } 50 | 51 | guard let resultString = String(data: data, encoding: .utf8), 52 | let resultStringData = resultString.data(using: .utf8) else { 53 | return .error(RequestError.unparseableJSON) 54 | } 55 | 56 | guard let jsonObject = try? serialization.jsonObject(with: resultStringData, options: []) else { 57 | return .okay(resultString) 58 | } 59 | 60 | return .okay(jsonObject) 61 | } 62 | } 63 | 64 | extension JSONRequestOperation { 65 | 66 | public class func create() -> JSONRequestOperation { 67 | let serialization = JSONSerialization.self 68 | let operation = JSONRequestOperation(serialization: serialization) 69 | return operation 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Pyrobase/RequestPath.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestPath.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 02/05/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | public protocol RequestPathProtocol { 10 | 11 | var baseURL: String { get } 12 | 13 | func build(_ path: String) -> String 14 | } 15 | 16 | public class RequestPath: RequestPathProtocol { 17 | 18 | internal var accessToken: String 19 | 20 | public var baseURL: String 21 | 22 | public init(baseURL: String, accessToken: String) { 23 | self.baseURL = baseURL 24 | self.accessToken = accessToken 25 | } 26 | 27 | public func build(_ path: String) -> String { 28 | guard !baseURL.isEmpty else { 29 | return "" 30 | } 31 | 32 | return "\(baseURL)/\(path).json?auth=\(accessToken)" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Pyrobase/RequestResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestResponse.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 22/06/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | public protocol RequestResponseProtocol { 10 | 11 | func isErroneous(_ response: HTTPURLResponse?, data: Data?) -> Error? 12 | } 13 | 14 | public class RequestResponse: RequestResponseProtocol { 15 | 16 | private(set) public var serializer: JSONSerialization.Type 17 | 18 | public init(serializer: JSONSerialization.Type = JSONSerialization.self) { 19 | self.serializer = serializer 20 | } 21 | 22 | public func isErroneous(_ response: HTTPURLResponse?, data: Data?) -> Error? { 23 | guard response != nil else { 24 | return nil 25 | } 26 | 27 | switch response!.statusCode { 28 | case 400: return RequestError.badRequest(errorMessage(data)) 29 | case 401: return RequestError.unauthorized(errorMessage(data)) 30 | case 403: return RequestError.forbidden(errorMessage(data)) 31 | case 404: return RequestError.notFound(errorMessage(data)) 32 | case 500: return RequestError.internalServiceError(errorMessage(data)) 33 | case 503: return RequestError.serviceUnavailable(errorMessage(data)) 34 | default: return nil 35 | } 36 | } 37 | 38 | func errorMessage(_ data: Data?) -> String { 39 | guard data != nil else { 40 | return "" 41 | } 42 | 43 | let info = (try? serializer.jsonObject(with: data!, options: [])) as? [AnyHashable: Any] 44 | return (info?["error"] as? String) ?? "" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Pyrobase/RequestResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestResult.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 05/06/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | public enum RequestResult { 10 | 11 | case failed(Error) 12 | case succeeded(Any) 13 | } 14 | 15 | public enum RequestError: Error { 16 | 17 | case invalidURL 18 | case unparseableJSON 19 | case noURLResponse 20 | case nullJSON 21 | case unknown 22 | 23 | case badRequest(String) 24 | case unauthorized(String) 25 | case forbidden(String) 26 | case notFound(String) 27 | case internalServiceError(String) 28 | case serviceUnavailable(String) 29 | 30 | public var code: Int { 31 | switch self { 32 | case .invalidURL: return -9000 33 | case .unparseableJSON: return -9001 34 | case .noURLResponse: return -9002 35 | case .nullJSON: return -9003 36 | case .unknown: return -9004 37 | case .badRequest: return -9005 38 | case .unauthorized: return -9006 39 | case .forbidden: return -9007 40 | case .notFound: return -9008 41 | case .internalServiceError: return -9009 42 | case .serviceUnavailable: return -9010 43 | } 44 | } 45 | 46 | public var message: String { 47 | switch self { 48 | case .invalidURL: return "URL is invalid" 49 | case .unparseableJSON: return "Can not parse JSON" 50 | case .noURLResponse: return "No URL response" 51 | case .nullJSON: return "JSON is null" 52 | case .unknown: return "Unknown error encountered" 53 | 54 | case .badRequest(let message), 55 | .unauthorized(let message), 56 | .forbidden(let message), 57 | .notFound(let message), 58 | .internalServiceError(let message), 59 | .serviceUnavailable(let message): 60 | return message 61 | } 62 | } 63 | } 64 | 65 | extension RequestError: Equatable { 66 | 67 | public static func ==(lhs: RequestError, rhs: RequestError) -> Bool { 68 | return lhs.code == rhs.code && lhs.message == rhs.message 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /PyrobaseTests/AuthRequestMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthRequestMock.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 09/06/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | import Pyrobase 10 | 11 | class AuthRequestMock: RequestProtocol { 12 | 13 | var writeURLPath: String = "" 14 | var writeMethod: RequestMethod = .post 15 | var writeData: [AnyHashable: Any] = [:] 16 | 17 | func read(path: String, query: [AnyHashable : Any], completion: @escaping (RequestResult) -> Void) { 18 | 19 | } 20 | 21 | func write(path: String, method: RequestMethod, data: [AnyHashable : Any], completion: @escaping (RequestResult) -> Void) { 22 | writeURLPath = path 23 | writeMethod = method 24 | writeData = data 25 | completion(.succeeded(true)) 26 | } 27 | 28 | func delete(path: String, completion: @escaping (RequestResult) -> Void) { 29 | 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /PyrobaseTests/CalendarExtensionTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CalendarExtensionTest.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 20/06/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Pyrobase 11 | 12 | class CalendarExtensionTest: XCTestCase { 13 | 14 | func testElapsedSecondsComputation() { 15 | let now = Date() 16 | let calendar = Calendar.current 17 | 18 | var expectedSeconds = 2 19 | var components = DateComponents() 20 | components.second = expectedSeconds 21 | var fromDate = Calendar.current.date(byAdding: components, to: now)! 22 | var seconds = calendar.seconds(from: fromDate, to: now) 23 | XCTAssertEqual(expectedSeconds, seconds) 24 | 25 | expectedSeconds = -4 26 | components.second = expectedSeconds 27 | fromDate = Calendar.current.date(byAdding: components, to: now)! 28 | seconds = calendar.seconds(from: fromDate, to: now) 29 | XCTAssertEqual(expectedSeconds, seconds) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /PyrobaseTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /PyrobaseTests/JSONRequestOperationTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSONRequestOperationTest.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 07/06/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Pyrobase 11 | 12 | class JSONRequestOperationTest: XCTestCase { 13 | 14 | func testBuildForGetMethod() { 15 | let operation = JSONRequestOperation.create() 16 | let url = URL(string: "https://foo.firebaseio.com/.json?accessToken=12345")! 17 | let request = operation.build(url: url, method: .get, data: [:]) 18 | let headers = request.allHTTPHeaderFields! 19 | 20 | XCTAssertEqual(headers.count, 2) 21 | XCTAssertEqual(headers["Accept"], "application/json") 22 | XCTAssertEqual(headers["Content-Type"], "application/json") 23 | XCTAssertEqual(request.httpMethod, "GET") 24 | XCTAssertNil(request.httpBody) 25 | } 26 | 27 | func testBuildForPostMethod() { 28 | let operation = JSONRequestOperation.create() 29 | let url = URL(string: "https://foo.firebaseio.com/.json?accessToken=12345")! 30 | var request = operation.build(url: url, method: .post, data: [:]) 31 | let headers = request.allHTTPHeaderFields! 32 | 33 | XCTAssertEqual(headers.count, 2) 34 | XCTAssertEqual(headers["Accept"], "application/json") 35 | XCTAssertEqual(headers["Content-Type"], "application/json") 36 | XCTAssertEqual(request.httpMethod, "POST") 37 | XCTAssertNil(request.httpBody) 38 | 39 | request = operation.build(url: url, method: .post, data: ["email": "me@me.com"]) 40 | XCTAssertNotNil(request.httpBody) 41 | } 42 | 43 | func testBuildForPutMethod() { 44 | let operation = JSONRequestOperation.create() 45 | let url = URL(string: "https://foo.firebaseio.com/.json?accessToken=12345")! 46 | var request = operation.build(url: url, method: .put, data: [:]) 47 | let headers = request.allHTTPHeaderFields! 48 | 49 | XCTAssertEqual(headers.count, 2) 50 | XCTAssertEqual(headers["Accept"], "application/json") 51 | XCTAssertEqual(headers["Content-Type"], "application/json") 52 | XCTAssertEqual(request.httpMethod, "PUT") 53 | XCTAssertNil(request.httpBody) 54 | 55 | request = operation.build(url: url, method: .put, data: ["email": "me@me.com"]) 56 | XCTAssertNotNil(request.httpBody) 57 | } 58 | 59 | func testBuildForPatchMethod() { 60 | let operation = JSONRequestOperation.create() 61 | let url = URL(string: "https://foo.firebaseio.com/.json?accessToken=12345")! 62 | var request = operation.build(url: url, method: .patch, data: [:]) 63 | let headers = request.allHTTPHeaderFields! 64 | 65 | XCTAssertEqual(headers.count, 2) 66 | XCTAssertEqual(headers["Accept"], "application/json") 67 | XCTAssertEqual(headers["Content-Type"], "application/json") 68 | XCTAssertEqual(request.httpMethod, "PATCH") 69 | XCTAssertNil(request.httpBody) 70 | 71 | request = operation.build(url: url, method: .patch, data: ["email": "me@me.com"]) 72 | XCTAssertNotNil(request.httpBody) 73 | } 74 | 75 | func testParse() { 76 | let operation = JSONRequestOperation.create() 77 | let param: [AnyHashable: Any] = ["email": "me@me.com"] 78 | let data = try? JSONSerialization.data(withJSONObject: param, options: []) 79 | let result = operation.parse(data: data!) 80 | 81 | switch result { 82 | case .error: 83 | XCTFail() 84 | 85 | case .okay(let info): 86 | XCTAssertTrue(info is [AnyHashable: Any]) 87 | let resultInfo = info as! [AnyHashable: Any] 88 | XCTAssertEqual(resultInfo.count, 1) 89 | XCTAssertEqual(param["email"] as! String, resultInfo["email"] as! String) 90 | } 91 | } 92 | 93 | func testParseWithValidJSONObjectButHavingNilConvertedResult() { 94 | let mock = JSONSerializationMock.self 95 | mock.isValid = true 96 | mock.shouldThrowErrorOnJSONObjectConversion = true 97 | let operation = JSONRequestOperation(serialization: mock) 98 | let param: [AnyHashable: Any] = ["email": "me@me.com"] 99 | let data = try? JSONSerialization.data(withJSONObject: param, options: []) 100 | let result = operation.parse(data: data!) 101 | 102 | switch result { 103 | case .okay: XCTFail() 104 | case .error: break 105 | } 106 | } 107 | 108 | func testParseWithValidJSONObjectAndHavingNonNilConvertedResult() { 109 | let mock = JSONSerializationMock.self 110 | mock.isValid = true 111 | mock.shouldThrowErrorOnJSONObjectConversion = false 112 | mock.expectedJSONObject = ["message": "wee"] 113 | let operation = JSONRequestOperation(serialization: mock) 114 | let param: [AnyHashable: Any] = ["email": "me@me.com"] 115 | let data = try? JSONSerialization.data(withJSONObject: param, options: []) 116 | let result = operation.parse(data: data!) 117 | 118 | switch result { 119 | case .error: XCTFail() 120 | case .okay: break 121 | } 122 | } 123 | 124 | func testParseWithNonValidJSONObjectButNotUTF8Encoded() { 125 | let operation = JSONRequestOperation.create() 126 | let string = "Jeprox" 127 | let data = string.data(using: .utf32) 128 | let result = operation.parse(data: data!) 129 | 130 | switch result { 131 | case .okay: 132 | XCTFail() 133 | 134 | case .error(let info): 135 | XCTAssertTrue(info is RequestError) 136 | let errorInfo = info as! RequestError 137 | XCTAssertTrue(errorInfo == RequestError.unparseableJSON) 138 | } 139 | } 140 | 141 | func testCreate() { 142 | let operation = JSONRequestOperation.create() 143 | XCTAssertTrue(operation.serialization == JSONSerialization.self) 144 | } 145 | 146 | func testBuildForGETWithData() { 147 | let operation = JSONRequestOperation.create() 148 | let url = URL(string: "https://foo.firebaseio.com")! 149 | let request = operation.build(url: url, method: .get, data: ["data": 1]) 150 | XCTAssertNil(request.httpBody) 151 | } 152 | } 153 | 154 | 155 | -------------------------------------------------------------------------------- /PyrobaseTests/JSONSerializationMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSONSerializationMock.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 07/06/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class JSONSerializationMock: JSONSerialization { 12 | 13 | enum MockError: Error { 14 | 15 | case throwError1 16 | } 17 | 18 | static var isValid: Bool = true 19 | static var shouldThrowErrorOnJSONObjectConversion: Bool = true 20 | static var expectedJSONObject: Any = "" 21 | 22 | override class func isValidJSONObject(_ obj: Any) -> Bool { 23 | return true 24 | } 25 | 26 | override class func jsonObject(with data: Data, options opt: JSONSerialization.ReadingOptions = []) throws -> Any { 27 | guard shouldThrowErrorOnJSONObjectConversion else { 28 | return expectedJSONObject 29 | } 30 | 31 | throw MockError.throwError1 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /PyrobaseTests/PlistReaderContentMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlistReaderContentMock.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 09/06/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | @testable import Pyrobase 10 | 11 | class PlistReaderContentMock: PlistReaderContentProtocol { 12 | 13 | var expectedData: Data? 14 | 15 | func extract(at path: String) -> Data? { 16 | return expectedData 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /PyrobaseTests/PlistReaderResourceMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BundleMock.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 09/06/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | @testable import Pyrobase 10 | 11 | class PlistReaderResourceMock: PlistReaderResourceProtocol { 12 | 13 | var expectedPath: String? 14 | 15 | func path(for name: String) -> String? { 16 | return expectedPath 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /PyrobaseTests/PlistReaderSample.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | Nina 7 | 8 | 9 | -------------------------------------------------------------------------------- /PyrobaseTests/PlistReaderTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlistReaderTest.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 09/06/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Pyrobase 11 | 12 | class PlistReaderTest: XCTestCase { 13 | 14 | func testInitWithErrorNotFound() { 15 | let resource = PlistReaderResourceMock() 16 | let content = PlistReaderContentMock() 17 | let format = PropertyListSerialization.PropertyListFormat.xml 18 | let serialization = PropertyListSerialization.self 19 | let isSucceeded: Bool 20 | 21 | resource.expectedPath = nil 22 | 23 | do { 24 | let _ = try PlistReader(name: "", resource: resource, content: content, format: format, serialization: serialization) 25 | isSucceeded = false 26 | 27 | } catch PlistReaderError.notFound { 28 | isSucceeded = true 29 | 30 | } catch { 31 | isSucceeded = false 32 | } 33 | 34 | XCTAssertTrue(isSucceeded) 35 | } 36 | 37 | func testInitWithErrorNoContent() { 38 | let resource = PlistReaderResourceMock() 39 | let content = PlistReaderContentMock() 40 | let format = PropertyListSerialization.PropertyListFormat.xml 41 | let serialization = PropertyListSerialization.self 42 | let isSucceeded: Bool 43 | 44 | resource.expectedPath = "path" 45 | content.expectedData = nil 46 | 47 | do { 48 | let _ = try PlistReader(name: "", resource: resource, content: content, format: format, serialization: serialization) 49 | isSucceeded = false 50 | 51 | } catch PlistReaderError.noContent { 52 | isSucceeded = true 53 | 54 | } catch { 55 | isSucceeded = false 56 | } 57 | 58 | XCTAssertTrue(isSucceeded) 59 | } 60 | 61 | func testInitWithErrorUnreadable() { 62 | let resource = PlistReaderResourceMock() 63 | let content = PlistReaderContentMock() 64 | let format = PropertyListSerialization.PropertyListFormat.xml 65 | let serialization = PlistSerializationMock.self 66 | let isSucceeded: Bool 67 | 68 | resource.expectedPath = "path" 69 | content.expectedData = Data(bytes: [1,2,3]) 70 | serialization.expectedInfo = nil 71 | 72 | do { 73 | let _ = try PlistReader(name: "", resource: resource, content: content, format: format, serialization: serialization) 74 | isSucceeded = false 75 | 76 | } catch PlistReaderError.unreadable { 77 | isSucceeded = true 78 | 79 | } catch { 80 | isSucceeded = false 81 | } 82 | 83 | XCTAssertTrue(isSucceeded) 84 | } 85 | 86 | func testInitWithNoErrors() { 87 | let resource = PlistReaderResourceMock() 88 | let content = PlistReaderContentMock() 89 | let format = PropertyListSerialization.PropertyListFormat.xml 90 | let serialization = PlistSerializationMock.self 91 | let isSucceeded: Bool 92 | 93 | resource.expectedPath = "path" 94 | content.expectedData = Data(bytes: [1,2,3]) 95 | serialization.expectedInfo = ["name": "Nina"] 96 | 97 | do { 98 | let reader = try PlistReader(name: "", resource: resource, content: content, format: format, serialization: serialization) 99 | XCTAssertNotNil(reader.data) 100 | XCTAssertTrue(reader.data is [String: String]) 101 | let readerInfo = reader.data as! [String: String] 102 | XCTAssertEqual(readerInfo.count, 1) 103 | XCTAssertEqual(readerInfo["name"], "Nina") 104 | isSucceeded = true 105 | 106 | } catch { 107 | isSucceeded = false 108 | } 109 | 110 | XCTAssertTrue(isSucceeded) 111 | } 112 | 113 | func testCreate() { 114 | let bundle = Bundle(for: type(of: self)) 115 | var reader = PlistReader.create(name: "PlistReaderSample", bundle: bundle) 116 | XCTAssertNotNil(reader) 117 | 118 | reader = PlistReader.create(name: "PlistReader-NonExisting") 119 | XCTAssertNil(reader) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /PyrobaseTests/PlistSerializationMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlistSerializationMock.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 09/06/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class PlistSerializationMock: PropertyListSerialization { 12 | 13 | enum MockError: Error { 14 | 15 | case nilExpectedInfo 16 | } 17 | 18 | static var expectedInfo: Any? 19 | 20 | override class func propertyList(from data: Data, options opt: PropertyListSerialization.ReadOptions = [], format: UnsafeMutablePointer?) throws -> Any { 21 | guard expectedInfo != nil else { 22 | throw MockError.nilExpectedInfo 23 | } 24 | 25 | return expectedInfo! 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /PyrobaseTests/PyroAuthContentTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PyroAuthContentTest.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 13/06/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Pyrobase 11 | 12 | class PyroAuthContentTest: XCTestCase { 13 | 14 | func testDescription() { 15 | let accessToken = "accessToken" 16 | let expiration = "3600" 17 | let refreshToken = "refreshToken" 18 | let userId = "me12345" 19 | let email = "me@me.com" 20 | 21 | var content = PyroAuthContent() 22 | content.accessToken = accessToken 23 | content.expiration = expiration 24 | content.refreshToken = refreshToken 25 | content.userId = userId 26 | content.email = email 27 | 28 | let expectedDescription = "userId: \(userId)\naccessToken: \(accessToken)\nemail: \(email)\nrefreshToken: \(refreshToken)\nexpiration: \(expiration)" 29 | XCTAssertEqual(content.description, expectedDescription) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /PyrobaseTests/PyroAuthMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PyroAuthMock.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 14/06/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | @testable import Pyrobase 10 | 11 | class PyroAuthMock: PyroAuth { 12 | 13 | static var defaultRequest: RequestProtocol = Request.create() 14 | static var defaultBundleIdentifier: String = "" 15 | static var defaultPlistName: String = "" 16 | 17 | override class func create(key: String, bundleIdentifier: String = "com.ner.Pyrobase", plistName: String = "PyroAuthInfo", request: RequestProtocol = Request.create() ) -> PyroAuth? { 18 | self.defaultRequest = request 19 | self.defaultBundleIdentifier = bundleIdentifier 20 | self.defaultPlistName = plistName 21 | 22 | return super.create(key: key, bundleIdentifier: bundleIdentifier, plistName: plistName, request: request) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /PyrobaseTests/PyroAuthTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PyroAuthTest.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 09/06/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Pyrobase 11 | 12 | class PyroAuthTest: XCTestCase { 13 | 14 | func testCreate() { 15 | let key: String = "api_key" 16 | 17 | var bundle: Bundle = .main 18 | var bundleIdentifier: String = bundle.bundleIdentifier ?? "" 19 | var auth = PyroAuth.create(key: key, bundleIdentifier: bundleIdentifier) 20 | XCTAssertNil(auth) // This is nil because Bundle.main's path is different when testing 21 | 22 | auth = PyroAuth.create(key: key) 23 | XCTAssertNotNil(auth) 24 | XCTAssertEqual(auth!.registerPath, "https://www.googleapis.com/identitytoolkit/v3/relyingparty/signupNewUser") 25 | XCTAssertEqual(auth!.signInPath, "https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword") 26 | XCTAssertEqual(auth!.refreshPath, "https://securetoken.googleapis.com/v1/token") 27 | XCTAssertEqual(auth!.confirmationCodePath, "https://www.googleapis.com/identitytoolkit/v3/relyingparty/getOobConfirmationCode") 28 | 29 | bundle = Bundle(for: type(of: self)) 30 | bundleIdentifier = bundle.bundleIdentifier ?? "" 31 | auth = PyroAuth.create(key: key, bundleIdentifier: bundleIdentifier, plistName: "PlistReaderSample") 32 | XCTAssertTrue(auth!.signInPath.isEmpty) 33 | XCTAssertTrue(auth!.registerPath.isEmpty) 34 | XCTAssertTrue(auth!.refreshPath.isEmpty) 35 | XCTAssertTrue(auth!.confirmationCodePath.isEmpty) 36 | 37 | auth = PyroAuth.create(key: key, bundleIdentifier: bundleIdentifier, plistName: "Plist-NonExisting") 38 | XCTAssertNil(auth) 39 | 40 | auth = PyroAuthMock.create(key: key) 41 | XCTAssertNotNil(auth) 42 | XCTAssertTrue(PyroAuthMock.defaultRequest is Request) 43 | XCTAssertEqual(PyroAuthMock.defaultPlistName, "PyroAuthInfo") 44 | XCTAssertEqual(PyroAuthMock.defaultBundleIdentifier, "com.ner.Pyrobase") 45 | } 46 | 47 | func testRegisterBeforeRequestIsTriggered() { 48 | let apiKey = "api_key" 49 | let email: String = "me@me.com" 50 | let password: String = "12345" 51 | 52 | let request = AuthRequestMock() 53 | let auth = PyroAuth.create(key: apiKey, request: request)! 54 | 55 | let expectation1 = expectation(description: "testRegister") 56 | 57 | auth.register(email: email, password: password) { _ in 58 | let expectedWriteData: [AnyHashable: Any] = [ 59 | "email": email, 60 | "password": password, 61 | "returnSecureToken": true 62 | ] 63 | 64 | let expectedWriteURLPath: String = "\(auth.registerPath)?key=\(apiKey)" 65 | let expectedMethod: RequestMethod = .post 66 | 67 | XCTAssertEqual(expectedWriteData.count, request.writeData.count) 68 | XCTAssertEqual(expectedWriteData["email"] as! String, request.writeData["email"] as! String) 69 | XCTAssertEqual(expectedWriteData["password"] as! String, request.writeData["password"] as! String) 70 | XCTAssertEqual(expectedWriteData["returnSecureToken"] as! Bool, request.writeData["returnSecureToken"] as! Bool) 71 | XCTAssertEqual(expectedWriteURLPath, request.writeURLPath) 72 | XCTAssertTrue(expectedMethod == request.writeMethod) 73 | 74 | expectation1.fulfill() 75 | } 76 | 77 | waitForExpectations(timeout: 1) 78 | } 79 | 80 | func testRegisterWithNoError() { 81 | let apiKey = "api_key" 82 | let email: String = "me@me.com" 83 | let password: String = "12345" 84 | 85 | let session = URLSessionMock() 86 | let operation = JSONRequestOperation.create() 87 | let response = RequestResponse() 88 | let request = Request(session: session, operation: operation, response: response) 89 | let auth = PyroAuth.create(key: apiKey, request: request)! 90 | 91 | let taskResult = URLSessionDataTaskMock.Result() 92 | let expectation1 = expectation(description: "testRegister") 93 | let expectedContent = [ 94 | "email": email, 95 | "idToken": "poiuy1820ghjfk", 96 | "localId": "userId12345", 97 | "refreshToken": "qwert8907zxcv", 98 | "expiresIn": "3600" 99 | ] 100 | 101 | taskResult.response = URLResponse() 102 | taskResult.data = try? JSONSerialization.data(withJSONObject: expectedContent, options: []) 103 | session.expectedDataTaskResult = taskResult 104 | 105 | auth.register(email: email, password: password) { result in 106 | switch result { 107 | case .failed: 108 | XCTFail() 109 | 110 | case .succeeded(let content): 111 | XCTAssertFalse(content.userId.isEmpty) 112 | XCTAssertFalse(content.email.isEmpty) 113 | XCTAssertFalse(content.accessToken.isEmpty) 114 | XCTAssertFalse(content.refreshToken.isEmpty) 115 | XCTAssertFalse(content.expiration.isEmpty) 116 | 117 | XCTAssertEqual(content.userId, expectedContent["localId"]) 118 | XCTAssertEqual(content.email, expectedContent["email"]) 119 | XCTAssertEqual(content.accessToken, expectedContent["idToken"]) 120 | XCTAssertEqual(content.refreshToken, expectedContent["refreshToken"]) 121 | XCTAssertEqual(content.expiration, expectedContent["expiresIn"]) 122 | } 123 | expectation1.fulfill() 124 | } 125 | 126 | waitForExpectations(timeout: 1) 127 | } 128 | 129 | func testRegisterWithUnexpectedContentError() { 130 | let apiKey = "api_key" 131 | let email: String = "me@me.com" 132 | let password: String = "12345" 133 | 134 | let session = URLSessionMock() 135 | let operation = JSONRequestOperation.create() 136 | let response = RequestResponse() 137 | let request = Request(session: session, operation: operation, response: response) 138 | let auth = PyroAuth.create(key: apiKey, request: request)! 139 | 140 | let taskResult = URLSessionDataTaskMock.Result() 141 | let expectation1 = expectation(description: "testRegister") 142 | 143 | taskResult.response = URLResponse() 144 | taskResult.data = "success".data(using: .utf8) 145 | session.expectedDataTaskResult = taskResult 146 | 147 | auth.register(email: email, password: password) { result in 148 | switch result { 149 | case .succeeded: 150 | XCTFail() 151 | 152 | case .failed(let info): 153 | XCTAssertTrue(info is PyroAuthError) 154 | let errorInfo = info as! PyroAuthError 155 | XCTAssertTrue(errorInfo == PyroAuthError.unexpectedContent) 156 | } 157 | expectation1.fulfill() 158 | } 159 | 160 | waitForExpectations(timeout: 1) 161 | } 162 | 163 | func testRegisterWithIncompleteContentError() { 164 | let apiKey = "api_key" 165 | let email: String = "me@me.com" 166 | let password: String = "12345" 167 | 168 | let session = URLSessionMock() 169 | let operation = JSONRequestOperation.create() 170 | let response = RequestResponse() 171 | let request = Request(session: session, operation: operation, response: response) 172 | let auth = PyroAuth.create(key: apiKey, request: request)! 173 | 174 | let taskResult = URLSessionDataTaskMock.Result() 175 | let expectation1 = expectation(description: "testRegister") 176 | let expectedContent = [ 177 | "email": email, 178 | "idToken": "poiuy1820ghjfk", 179 | ] 180 | 181 | taskResult.response = URLResponse() 182 | taskResult.data = try? JSONSerialization.data(withJSONObject: expectedContent, options: []) 183 | session.expectedDataTaskResult = taskResult 184 | 185 | auth.register(email: email, password: password) { result in 186 | switch result { 187 | case .succeeded: 188 | XCTFail() 189 | 190 | case .failed(let info): 191 | XCTAssertTrue(info is PyroAuthError) 192 | let errorInfo = info as! PyroAuthError 193 | XCTAssertTrue(errorInfo == PyroAuthError.incompleteContent) 194 | } 195 | expectation1.fulfill() 196 | } 197 | 198 | waitForExpectations(timeout: 1) 199 | } 200 | 201 | func testRegisterWithCustomError() { 202 | let apiKey = "api_key" 203 | let email: String = "me@me.com" 204 | let password: String = "12345" 205 | 206 | let session = URLSessionMock() 207 | let operation = JSONRequestOperation.create() 208 | let response = RequestResponse() 209 | let request = Request(session: session, operation: operation, response: response) 210 | let auth = PyroAuth.create(key: apiKey, request: request)! 211 | 212 | let taskResult = URLSessionDataTaskMock.Result() 213 | let expectation1 = expectation(description: "testRegister") 214 | 215 | taskResult.error = URLSessionDataTaskMock.TaskMockError.mockError1 216 | session.expectedDataTaskResult = taskResult 217 | 218 | auth.register(email: email, password: password) { result in 219 | switch result { 220 | case .succeeded: 221 | XCTFail() 222 | 223 | case .failed(let info): 224 | XCTAssertTrue(info is URLSessionDataTaskMock.TaskMockError) 225 | let errorInfo = info as! URLSessionDataTaskMock.TaskMockError 226 | XCTAssertTrue(errorInfo == URLSessionDataTaskMock.TaskMockError.mockError1) 227 | } 228 | expectation1.fulfill() 229 | } 230 | 231 | waitForExpectations(timeout: 1) 232 | } 233 | 234 | func testSignInBeforeRequestIsTriggered() { 235 | let apiKey = "api_key" 236 | let email: String = "me@me.com" 237 | let password: String = "12345" 238 | 239 | let request = AuthRequestMock() 240 | let auth = PyroAuth.create(key: apiKey, request: request)! 241 | 242 | let expectation1 = expectation(description: "testSignIn") 243 | 244 | auth.signIn(email: email, password: password) { _ in 245 | let expectedWriteData: [AnyHashable: Any] = [ 246 | "email": email, 247 | "password": password, 248 | "returnSecureToken": true 249 | ] 250 | 251 | let expectedWriteURLPath: String = "\(auth.signInPath)?key=\(apiKey)" 252 | let expectedMethod: RequestMethod = .post 253 | 254 | XCTAssertEqual(expectedWriteData.count, request.writeData.count) 255 | XCTAssertEqual(expectedWriteData["email"] as! String, request.writeData["email"] as! String) 256 | XCTAssertEqual(expectedWriteData["password"] as! String, request.writeData["password"] as! String) 257 | XCTAssertEqual(expectedWriteData["returnSecureToken"] as! Bool, request.writeData["returnSecureToken"] as! Bool) 258 | XCTAssertEqual(expectedWriteURLPath, request.writeURLPath) 259 | XCTAssertTrue(expectedMethod == request.writeMethod) 260 | 261 | expectation1.fulfill() 262 | } 263 | 264 | waitForExpectations(timeout: 1) 265 | } 266 | 267 | func testSignInWithNoError() { 268 | let apiKey = "api_key" 269 | let email: String = "me@me.com" 270 | let password: String = "12345" 271 | 272 | let session = URLSessionMock() 273 | let operation = JSONRequestOperation.create() 274 | let response = RequestResponse() 275 | let request = Request(session: session, operation: operation, response: response) 276 | let auth = PyroAuth.create(key: apiKey, request: request)! 277 | 278 | let taskResult = URLSessionDataTaskMock.Result() 279 | let expectation1 = expectation(description: "testSignIn") 280 | let expectedContent = [ 281 | "email": email, 282 | "idToken": "poiuy1820ghjfk", 283 | "localId": "userId12345", 284 | "refreshToken": "qwert8907zxcv", 285 | "expiresIn": "3600" 286 | ] 287 | 288 | taskResult.response = URLResponse() 289 | taskResult.data = try? JSONSerialization.data(withJSONObject: expectedContent, options: []) 290 | session.expectedDataTaskResult = taskResult 291 | 292 | auth.signIn(email: email, password: password) { result in 293 | switch result { 294 | case .failed: 295 | XCTFail() 296 | 297 | case .succeeded(let content): 298 | XCTAssertFalse(content.userId.isEmpty) 299 | XCTAssertFalse(content.email.isEmpty) 300 | XCTAssertFalse(content.accessToken.isEmpty) 301 | XCTAssertFalse(content.refreshToken.isEmpty) 302 | XCTAssertFalse(content.expiration.isEmpty) 303 | 304 | XCTAssertEqual(content.userId, expectedContent["localId"]) 305 | XCTAssertEqual(content.email, expectedContent["email"]) 306 | XCTAssertEqual(content.accessToken, expectedContent["idToken"]) 307 | XCTAssertEqual(content.refreshToken, expectedContent["refreshToken"]) 308 | XCTAssertEqual(content.expiration, expectedContent["expiresIn"]) 309 | } 310 | expectation1.fulfill() 311 | } 312 | 313 | waitForExpectations(timeout: 1) 314 | } 315 | 316 | func testSignInWithUnexpectedContentError() { 317 | let apiKey = "api_key" 318 | let email: String = "me@me.com" 319 | let password: String = "12345" 320 | 321 | let session = URLSessionMock() 322 | let operation = JSONRequestOperation.create() 323 | let response = RequestResponse() 324 | let request = Request(session: session, operation: operation, response: response) 325 | let auth = PyroAuth.create(key: apiKey, request: request)! 326 | 327 | let taskResult = URLSessionDataTaskMock.Result() 328 | let expectation1 = expectation(description: "testSignIn") 329 | 330 | taskResult.response = URLResponse() 331 | taskResult.data = "success".data(using: .utf8) 332 | session.expectedDataTaskResult = taskResult 333 | 334 | auth.signIn(email: email, password: password) { result in 335 | switch result { 336 | case .succeeded: 337 | XCTFail() 338 | 339 | case .failed(let info): 340 | XCTAssertTrue(info is PyroAuthError) 341 | let errorInfo = info as! PyroAuthError 342 | XCTAssertTrue(errorInfo == PyroAuthError.unexpectedContent) 343 | } 344 | expectation1.fulfill() 345 | } 346 | 347 | waitForExpectations(timeout: 1) 348 | } 349 | 350 | func testSignInWithIncompleteContentError() { 351 | let apiKey = "api_key" 352 | let email: String = "me@me.com" 353 | let password: String = "12345" 354 | 355 | let session = URLSessionMock() 356 | let operation = JSONRequestOperation.create() 357 | let response = RequestResponse() 358 | let request = Request(session: session, operation: operation, response: response) 359 | let auth = PyroAuth.create(key: apiKey, request: request)! 360 | 361 | let taskResult = URLSessionDataTaskMock.Result() 362 | let expectation1 = expectation(description: "testSignIn") 363 | let expectedContent = [ 364 | "email": email, 365 | "idToken": "poiuy1820ghjfk", 366 | ] 367 | 368 | taskResult.response = URLResponse() 369 | taskResult.data = try? JSONSerialization.data(withJSONObject: expectedContent, options: []) 370 | session.expectedDataTaskResult = taskResult 371 | 372 | auth.signIn(email: email, password: password) { result in 373 | switch result { 374 | case .succeeded: 375 | XCTFail() 376 | 377 | case .failed(let info): 378 | XCTAssertTrue(info is PyroAuthError) 379 | let errorInfo = info as! PyroAuthError 380 | XCTAssertTrue(errorInfo == PyroAuthError.incompleteContent) 381 | } 382 | expectation1.fulfill() 383 | } 384 | 385 | waitForExpectations(timeout: 1) 386 | } 387 | 388 | func testSignInWithCustomError() { 389 | let apiKey = "api_key" 390 | let email: String = "me@me.com" 391 | let password: String = "12345" 392 | 393 | let session = URLSessionMock() 394 | let operation = JSONRequestOperation.create() 395 | let response = RequestResponse() 396 | let request = Request(session: session, operation: operation, response: response) 397 | let auth = PyroAuth.create(key: apiKey, request: request)! 398 | 399 | let taskResult = URLSessionDataTaskMock.Result() 400 | let expectation1 = expectation(description: "testSignIn") 401 | 402 | taskResult.error = URLSessionDataTaskMock.TaskMockError.mockError1 403 | session.expectedDataTaskResult = taskResult 404 | 405 | auth.signIn(email: email, password: password) { result in 406 | switch result { 407 | case .succeeded: 408 | XCTFail() 409 | 410 | case .failed(let info): 411 | XCTAssertTrue(info is URLSessionDataTaskMock.TaskMockError) 412 | let errorInfo = info as! URLSessionDataTaskMock.TaskMockError 413 | XCTAssertTrue(errorInfo == URLSessionDataTaskMock.TaskMockError.mockError1) 414 | } 415 | expectation1.fulfill() 416 | } 417 | 418 | waitForExpectations(timeout: 1) 419 | } 420 | 421 | func testRefreshBeforeRequestIsTriggered() { 422 | let apiKey = "api_key" 423 | let refreshToken: String = "refresh_token" 424 | 425 | let request = AuthRequestMock() 426 | let auth = PyroAuth.create(key: apiKey, request: request)! 427 | 428 | let expectation1 = expectation(description: "testRefresh") 429 | 430 | auth.refresh(token: refreshToken) { _ in 431 | let expectedWriteData: [AnyHashable: Any] = [ 432 | "grant_type": "refresh_token", 433 | "refresh_token": refreshToken 434 | ] 435 | 436 | let expectedWriteURLPath: String = "\(auth.refreshPath)?key=\(apiKey)" 437 | let expectedMethod: RequestMethod = .post 438 | 439 | XCTAssertEqual(expectedWriteData.count, request.writeData.count) 440 | XCTAssertEqual(expectedWriteData["grant_type"] as! String, request.writeData["grant_type"] as! String) 441 | XCTAssertEqual(expectedWriteData["refresh_token"] as! String, request.writeData["refresh_token"] as! String) 442 | XCTAssertEqual(expectedWriteURLPath, request.writeURLPath) 443 | XCTAssertTrue(expectedMethod == request.writeMethod) 444 | 445 | expectation1.fulfill() 446 | } 447 | 448 | waitForExpectations(timeout: 1) 449 | } 450 | 451 | func testRefreshWithNoError() { 452 | let apiKey = "api_key" 453 | let refreshToken = "refresh_token" 454 | 455 | let session = URLSessionMock() 456 | let operation = JSONRequestOperation.create() 457 | let response = RequestResponse() 458 | let request = Request(session: session, operation: operation, response: response) 459 | let auth = PyroAuth.create(key: apiKey, request: request)! 460 | 461 | let taskResult = URLSessionDataTaskMock.Result() 462 | let expectation1 = expectation(description: "testRefresh") 463 | let expectedContent = [ 464 | "access_token": "poiuy1820ghjfk", 465 | "refresh_token": "qwert8907zxcv", 466 | "expires_in": "3600" 467 | ] 468 | 469 | taskResult.response = URLResponse() 470 | taskResult.data = try? JSONSerialization.data(withJSONObject: expectedContent, options: []) 471 | session.expectedDataTaskResult = taskResult 472 | 473 | auth.refresh(token: refreshToken) { result in 474 | switch result { 475 | case .failed: 476 | XCTFail() 477 | 478 | case .succeeded(let content): 479 | XCTAssertFalse(content.accessToken.isEmpty) 480 | XCTAssertFalse(content.refreshToken.isEmpty) 481 | XCTAssertFalse(content.expiration.isEmpty) 482 | 483 | XCTAssertEqual(content.accessToken, expectedContent["access_token"]) 484 | XCTAssertEqual(content.refreshToken, expectedContent["refresh_token"]) 485 | XCTAssertEqual(content.expiration, expectedContent["expires_in"]) 486 | } 487 | expectation1.fulfill() 488 | } 489 | 490 | waitForExpectations(timeout: 1) 491 | } 492 | 493 | func testRefreshWithUnexpectedContentError() { 494 | let apiKey = "api_key" 495 | let refreshToken = "refresh_token" 496 | 497 | let session = URLSessionMock() 498 | let operation = JSONRequestOperation.create() 499 | let response = RequestResponse() 500 | let request = Request(session: session, operation: operation, response: response) 501 | let auth = PyroAuth.create(key: apiKey, request: request)! 502 | 503 | let taskResult = URLSessionDataTaskMock.Result() 504 | let expectation1 = expectation(description: "testRefresh") 505 | 506 | taskResult.response = URLResponse() 507 | taskResult.data = "success".data(using: .utf8) 508 | session.expectedDataTaskResult = taskResult 509 | 510 | auth.refresh(token: refreshToken) { result in 511 | switch result { 512 | case .succeeded: 513 | XCTFail() 514 | 515 | case .failed(let info): 516 | XCTAssertTrue(info is PyroAuthError) 517 | let errorInfo = info as! PyroAuthError 518 | XCTAssertTrue(errorInfo == PyroAuthError.unexpectedContent) 519 | } 520 | expectation1.fulfill() 521 | } 522 | 523 | waitForExpectations(timeout: 1) 524 | } 525 | 526 | func testRefreshWithIncompleteContentError() { 527 | let apiKey = "api_key" 528 | let refreshToken = "refresh_token" 529 | 530 | let session = URLSessionMock() 531 | let operation = JSONRequestOperation.create() 532 | let response = RequestResponse() 533 | let request = Request(session: session, operation: operation, response: response) 534 | let auth = PyroAuth.create(key: apiKey, request: request)! 535 | 536 | let taskResult = URLSessionDataTaskMock.Result() 537 | let expectation1 = expectation(description: "testRefresh") 538 | let expectedContent = [ 539 | "access_token": "poiuy1820ghjfk", 540 | ] 541 | 542 | taskResult.response = URLResponse() 543 | taskResult.data = try? JSONSerialization.data(withJSONObject: expectedContent, options: []) 544 | session.expectedDataTaskResult = taskResult 545 | 546 | auth.refresh(token: refreshToken) { result in 547 | switch result { 548 | case .succeeded: 549 | XCTFail() 550 | 551 | case .failed(let info): 552 | XCTAssertTrue(info is PyroAuthError) 553 | let errorInfo = info as! PyroAuthError 554 | XCTAssertTrue(errorInfo == PyroAuthError.incompleteContent) 555 | } 556 | expectation1.fulfill() 557 | } 558 | 559 | waitForExpectations(timeout: 1) 560 | } 561 | 562 | func testRefreshWithCustomError() { 563 | let apiKey = "api_key" 564 | let refreshToken = "refresh_token" 565 | 566 | let session = URLSessionMock() 567 | let operation = JSONRequestOperation.create() 568 | let response = RequestResponse() 569 | let request = Request(session: session, operation: operation, response: response) 570 | let auth = PyroAuth.create(key: apiKey, request: request)! 571 | 572 | let taskResult = URLSessionDataTaskMock.Result() 573 | let expectation1 = expectation(description: "testRefresh") 574 | 575 | taskResult.error = URLSessionDataTaskMock.TaskMockError.mockError1 576 | session.expectedDataTaskResult = taskResult 577 | 578 | auth.refresh(token: refreshToken) { result in 579 | switch result { 580 | case .succeeded: 581 | XCTFail() 582 | 583 | case .failed(let info): 584 | XCTAssertTrue(info is URLSessionDataTaskMock.TaskMockError) 585 | let errorInfo = info as! URLSessionDataTaskMock.TaskMockError 586 | XCTAssertTrue(errorInfo == URLSessionDataTaskMock.TaskMockError.mockError1) 587 | } 588 | expectation1.fulfill() 589 | } 590 | 591 | waitForExpectations(timeout: 1) 592 | } 593 | 594 | func testSendPasswordResetBeforeRequestIsTriggered() { 595 | let apiKey = "api_key" 596 | let email: String = "me@me.com" 597 | 598 | let request = AuthRequestMock() 599 | let auth = PyroAuth.create(key: apiKey, request: request)! 600 | 601 | let expectation1 = expectation(description: "testSendPasswordReset") 602 | 603 | auth.sendPasswordReset(email: email) { _ in 604 | let expectedWriteData: [AnyHashable: Any] = [ 605 | "email": email, 606 | "requestType": "PASSWORD_RESET" 607 | ] 608 | 609 | let expectedWriteURLPath: String = "\(auth.confirmationCodePath)?key=\(apiKey)" 610 | let expectedMethod: RequestMethod = .post 611 | 612 | XCTAssertEqual(expectedWriteData.count, request.writeData.count) 613 | XCTAssertEqual(expectedWriteData["email"] as! String, request.writeData["email"] as! String) 614 | XCTAssertEqual(expectedWriteData["requestType"] as! String, request.writeData["requestType"] as! String) 615 | XCTAssertEqual(expectedWriteURLPath, request.writeURLPath) 616 | XCTAssertTrue(expectedMethod == request.writeMethod) 617 | 618 | expectation1.fulfill() 619 | } 620 | 621 | waitForExpectations(timeout: 1) 622 | } 623 | 624 | func testSendPasswordResetWithNoError() { 625 | let apiKey = "api_key" 626 | let email = "me@me.com" 627 | 628 | let session = URLSessionMock() 629 | let operation = JSONRequestOperation.create() 630 | let response = RequestResponse() 631 | let request = Request(session: session, operation: operation, response: response) 632 | let auth = PyroAuth.create(key: apiKey, request: request)! 633 | 634 | let taskResult = URLSessionDataTaskMock.Result() 635 | let expectation1 = expectation(description: "testSendPasswordReset") 636 | 637 | taskResult.response = URLResponse() 638 | taskResult.data = Data(bytes: [1,2,3]) 639 | session.expectedDataTaskResult = taskResult 640 | 641 | auth.sendPasswordReset(email: email) { result in 642 | switch result { 643 | case .failed: 644 | XCTFail() 645 | 646 | case .succeeded(let isSuccess): 647 | XCTAssertTrue(isSuccess) 648 | } 649 | expectation1.fulfill() 650 | } 651 | 652 | waitForExpectations(timeout: 1) 653 | } 654 | 655 | func testSendPasswordResetWithCustomError() { 656 | let apiKey = "api_key" 657 | let email = "me@me.com" 658 | 659 | let session = URLSessionMock() 660 | let operation = JSONRequestOperation.create() 661 | let response = RequestResponse() 662 | let request = Request(session: session, operation: operation, response: response) 663 | let auth = PyroAuth.create(key: apiKey, request: request)! 664 | 665 | let taskResult = URLSessionDataTaskMock.Result() 666 | let expectation1 = expectation(description: "testSendPasswordReset") 667 | 668 | taskResult.error = URLSessionDataTaskMock.TaskMockError.mockError1 669 | session.expectedDataTaskResult = taskResult 670 | 671 | auth.sendPasswordReset(email: email) { result in 672 | switch result { 673 | case .succeeded: 674 | XCTFail() 675 | 676 | case .failed(let info): 677 | XCTAssertTrue(info is URLSessionDataTaskMock.TaskMockError) 678 | let errorInfo = info as! URLSessionDataTaskMock.TaskMockError 679 | XCTAssertTrue(errorInfo == URLSessionDataTaskMock.TaskMockError.mockError1) 680 | } 681 | expectation1.fulfill() 682 | } 683 | 684 | waitForExpectations(timeout: 1) 685 | } 686 | 687 | } 688 | -------------------------------------------------------------------------------- /PyrobaseTests/PyroAuthTokenContentTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PyroAuthTokenContentTest.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 13/06/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Pyrobase 11 | 12 | class PyroAuthTokenContentTest: XCTestCase { 13 | 14 | func testDescription() { 15 | let accessToken = "accessToken" 16 | let expiration = "3600" 17 | let refreshToken = "refreshToken" 18 | 19 | var content = PyroAuthTokenContent() 20 | content.accessToken = accessToken 21 | content.expiration = expiration 22 | content.refreshToken = refreshToken 23 | 24 | let expectedDescription = "accessToken: \(accessToken)\nexpiration: \(expiration)\nrefreshToken: \(refreshToken)" 25 | XCTAssertEqual(content.description, expectedDescription) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /PyrobaseTests/PyroEventSourceMessageTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PyroEventSourceMessageTest.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 23/06/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Pyrobase 11 | 12 | class PyroEventSourceMessageTest: XCTestCase { 13 | 14 | func testDescription() { 15 | var message = PyroEventSourceMessage() 16 | message.id = "1" 17 | message.event = "put" 18 | message.data = "hello world" 19 | XCTAssertEqual(message.description, "\nid: 1\nevent: put\ndata: hello world\n") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PyrobaseTests/PyroEventSourceParserTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PyroEventSourceParserTest.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 23/06/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Pyrobase 11 | 12 | class PyroEventSourceParserTest: XCTestCase { 13 | 14 | func testParseWithDataNotUTF8Encoded() { 15 | let parser = PyroEventSourceParser() 16 | let data = "id: 1\nevent: put\ndata: hello world\n\n".data(using: .utf32)! 17 | let message = parser.parse(data) 18 | XCTAssertTrue(message.id.isEmpty) 19 | XCTAssertTrue(message.event.isEmpty) 20 | XCTAssertTrue(message.data.isEmpty) 21 | } 22 | 23 | func testParseWithEmptyEventString() { 24 | let parser = PyroEventSourceParser() 25 | let data = "\n\n\n\n".data(using: .utf8)! 26 | let message = parser.parse(data) 27 | XCTAssertTrue(message.id.isEmpty) 28 | XCTAssertTrue(message.event.isEmpty) 29 | XCTAssertTrue(message.data.isEmpty) 30 | } 31 | 32 | func testParseWithColonAsPrefix() { 33 | let parser = PyroEventSourceParser() 34 | let data = ":\n: wee\n: toinks\n\n".data(using: .utf8)! 35 | let message = parser.parse(data) 36 | XCTAssertTrue(message.id.isEmpty) 37 | XCTAssertTrue(message.event.isEmpty) 38 | XCTAssertTrue(message.data.isEmpty) 39 | } 40 | 41 | func testParseWithRetryString() { 42 | let parser = PyroEventSourceParser() 43 | var data = "retry: 3600\n\n".data(using: .utf8)! 44 | var message = parser.parse(data) 45 | XCTAssertTrue(message.id.isEmpty) 46 | XCTAssertTrue(message.event.isEmpty) 47 | XCTAssertTrue(message.data.isEmpty) 48 | 49 | data = "id: retry:\n\n".data(using: .utf8)! 50 | message = parser.parse(data) 51 | XCTAssertTrue(message.id.isEmpty) 52 | XCTAssertTrue(message.event.isEmpty) 53 | XCTAssertTrue(message.data.isEmpty) 54 | } 55 | 56 | func testParseWithNoColon() { 57 | let parser = PyroEventSourceParser() 58 | let data = "id\n\n".data(using: .utf8)! 59 | let message = parser.parse(data) 60 | XCTAssertTrue(message.id.isEmpty) 61 | XCTAssertTrue(message.event.isEmpty) 62 | XCTAssertTrue(message.data.isEmpty) 63 | } 64 | 65 | func testParseWithValidData() { 66 | let parser = PyroEventSourceParser() 67 | var data = "id: 1\nevent: put\ndata: hello world\n\n".data(using: .utf8)! 68 | var message = parser.parse(data) 69 | XCTAssertEqual(message.id, "1") 70 | XCTAssertEqual(message.event, "put") 71 | XCTAssertEqual(message.data, "hello world") 72 | 73 | data = "foo: 1\nbar: put\nwee: hello world\n\n".data(using: .utf8)! 74 | message = parser.parse(data) 75 | XCTAssertTrue(message.id.isEmpty) 76 | XCTAssertTrue(message.event.isEmpty) 77 | XCTAssertTrue(message.data.isEmpty) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /PyrobaseTests/PyroEventSourceSessionProviderMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PyroEventSourceSessionProviderMock.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 23/06/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | @testable import Pyrobase 10 | 11 | class PyroEventSourceSessionProviderMock: PyroEventSourceSessionProviderProtocol { 12 | 13 | var queue: OperationQueue = OperationQueue() 14 | var headers: [String: String] = [:] 15 | 16 | func createSession(for delegate: URLSessionDataDelegate, lastEventID: String) -> URLSession { 17 | let session = URLSessionMock() 18 | session.shouldExecuteTaskResume = false 19 | return session 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PyrobaseTests/PyroEventSourceSessionProviderTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PyroEventSourceSessionProviderTest.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 23/06/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Pyrobase 11 | 12 | class PyroEventSourceSessionProviderTest: XCTestCase { 13 | 14 | func testCreate() { 15 | let provider = PyroEventSourceSessionProvider.create() 16 | XCTAssertEqual(provider.headers.count, 1) 17 | let accept = provider.headers["Accept"] 18 | XCTAssertNotNil(accept) 19 | XCTAssertEqual(accept!, "text/event-stream") 20 | } 21 | 22 | func testCreateSession() { 23 | let delegate = URLSessionDataDelegateMock() 24 | let provider = PyroEventSourceSessionProvider.create() 25 | var session = provider.createSession(for: delegate, lastEventID: "") 26 | XCTAssertNotNil(session.configuration.httpAdditionalHeaders) 27 | XCTAssertEqual(session.configuration.httpAdditionalHeaders as! [String: String], provider.headers) 28 | XCTAssertEqual(session.configuration.timeoutIntervalForRequest, TimeInterval(INT_MAX)) 29 | XCTAssertEqual(session.configuration.timeoutIntervalForResource, TimeInterval(INT_MAX)) 30 | XCTAssertTrue(session.delegate is URLSessionDataDelegateMock) 31 | 32 | XCTAssertNil(session.configuration.httpAdditionalHeaders!["Last-Event-Id"]) 33 | 34 | let lastEventId = "12345" 35 | session = provider.createSession(for: delegate, lastEventID: lastEventId) 36 | XCTAssertNotNil(session.configuration.httpAdditionalHeaders!["Last-Event-Id"]) 37 | XCTAssertEqual(session.configuration.httpAdditionalHeaders!["Last-Event-Id"] as! String, lastEventId) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /PyrobaseTests/PyroEventSourceTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PyroEventSourceTest.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 21/06/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Pyrobase 11 | 12 | class PyroEventSourceTest: XCTestCase { 13 | 14 | var eventSource: PyroEventSource! 15 | 16 | var isCalledOpenCallback: Bool = false 17 | var isCalledClosedCallback: Bool = false 18 | var isCalledConnectingCallback: Bool = false 19 | 20 | var expectedPyroEventSourceError: PyroEventSourceError? 21 | var expectedRequestError: RequestError? 22 | var expectedMessage: PyroEventSourceMessage? 23 | 24 | override func setUp() { 25 | isCalledOpenCallback = false 26 | isCalledClosedCallback = false 27 | isCalledConnectingCallback = false 28 | } 29 | 30 | func testCreate() { 31 | let baseURL = "https://foo.firebaseio.com" 32 | let accessToken = "accessToken" 33 | eventSource = PyroEventSource.create(baseURL: baseURL, accessToken: accessToken) 34 | 35 | let path = eventSource.path as! RequestPath 36 | 37 | XCTAssertEqual(path.baseURL, baseURL) 38 | XCTAssertEqual(path.accessToken, accessToken) 39 | 40 | XCTAssertNil(eventSource.callback) 41 | XCTAssertNil(eventSource.session) 42 | XCTAssertTrue(eventSource.lastEventID.isEmpty) 43 | 44 | XCTAssertTrue(eventSource.parser is PyroEventSourceParser) 45 | XCTAssertTrue(eventSource.response is RequestResponse) 46 | XCTAssertTrue(eventSource.state == .closed) 47 | 48 | let lastEventID = "abcde12345qwert" 49 | eventSource = PyroEventSource.create(baseURL: baseURL, accessToken: accessToken, lastEventID: lastEventID) 50 | XCTAssertEqual(eventSource.lastEventID, lastEventID) 51 | } 52 | 53 | func testClose() { 54 | let baseURL = "https://foo.firebaseio.com" 55 | let accessToken = "accessToken" 56 | eventSource = PyroEventSource.create(baseURL: baseURL, accessToken: accessToken) 57 | 58 | eventSource.close() 59 | 60 | XCTAssertFalse(isCalledClosedCallback) 61 | XCTAssertNil(eventSource.session) 62 | XCTAssertTrue(eventSource.state == .closed) 63 | 64 | eventSource.callback = self 65 | eventSource.close() 66 | XCTAssertTrue(isCalledClosedCallback) 67 | } 68 | 69 | func testStateDidSetObserver() { 70 | let baseURL = "https://foo.firebaseio.com" 71 | let accessToken = "accessToken" 72 | eventSource = PyroEventSource.create(baseURL: baseURL, accessToken: accessToken) 73 | eventSource.callback = self 74 | eventSource.state = .open 75 | eventSource.state = .closed 76 | eventSource.state = .connecting 77 | 78 | XCTAssertTrue(isCalledOpenCallback) 79 | XCTAssertTrue(isCalledClosedCallback) 80 | XCTAssertTrue(isCalledConnectingCallback) 81 | } 82 | 83 | func testIsForcedClose() { 84 | let baseURL = "https://foo.firebaseio.com" 85 | let accessToken = "accessToken" 86 | eventSource = PyroEventSource.create(baseURL: baseURL, accessToken: accessToken) 87 | var httpResponse = HTTPURLResponse(url: URL(string: baseURL)!, statusCode: 204, httpVersion: nil, headerFields: nil) 88 | var isForcedClose = eventSource.isForcedClose(httpResponse) 89 | XCTAssertTrue(isForcedClose) 90 | 91 | httpResponse = HTTPURLResponse(url: URL(string: baseURL)!, statusCode: 200, httpVersion: nil, headerFields: nil) 92 | isForcedClose = eventSource.isForcedClose(httpResponse) 93 | XCTAssertFalse(isForcedClose) 94 | } 95 | 96 | func testStreamWithNotClosedError() { 97 | let baseURL = "https://foo.firebaseio.com" 98 | let accessToken = "accessToken" 99 | eventSource = PyroEventSource.create(baseURL: baseURL, accessToken: accessToken) 100 | eventSource.callback = self 101 | expectedPyroEventSourceError = PyroEventSourceError.notClosed 102 | 103 | eventSource.state = .open 104 | eventSource.stream("users/abcde12345qwert") 105 | 106 | eventSource.state = .connecting 107 | eventSource.stream("users/abcde12345qwert") 108 | } 109 | 110 | func testStreamWithInvalidURLError() { 111 | let accessToken = "accessToken" 112 | eventSource = PyroEventSource.create(baseURL: "", accessToken: accessToken) 113 | eventSource.callback = self 114 | expectedRequestError = RequestError.invalidURL 115 | 116 | eventSource.stream("users/abcde12345qwert") 117 | eventSource.stream("") 118 | } 119 | 120 | func testStreamWithConnectingState() { 121 | let baseURL = "https://foo.firebaseio.com" 122 | let accessToken = "accessToken" 123 | eventSource = PyroEventSource.create(baseURL: baseURL, accessToken: accessToken) 124 | eventSource.callback = self 125 | eventSource.stream("users/abcde12345qwert") 126 | XCTAssertTrue(eventSource.state == .connecting) 127 | XCTAssertNotNil(eventSource.session) 128 | } 129 | 130 | func testURLSessionDidReceiveDataWithNotOpenError() { 131 | let baseURL = "https://foo.firebaseio.com" 132 | let accessToken = "accessToken" 133 | let session = URLSession() 134 | let httpResponse = HTTPURLResponse(url: URL(string: "https://sampleio.com")!, statusCode: 200, httpVersion: nil, headerFields: nil) 135 | let task = URLSessionDataTaskMock(httpResponse: httpResponse) 136 | let data = Data(bytes: [0,1,1,2,3,5,8]) 137 | expectedPyroEventSourceError = PyroEventSourceError.notOpen 138 | eventSource = PyroEventSource.create(baseURL: baseURL, accessToken: accessToken) 139 | eventSource.callback = self 140 | eventSource.urlSession(session, dataTask: task, didReceive: data) 141 | } 142 | 143 | func testURLSessionDidReceiveDataWithForcedCloseError() { 144 | let baseURL = "https://foo.firebaseio.com" 145 | let accessToken = "accessToken" 146 | let session = URLSession() 147 | let httpResponse = HTTPURLResponse(url: URL(string: "https://sampleio.com")!, statusCode: 204, httpVersion: nil, headerFields: nil) 148 | let task = URLSessionDataTaskMock(httpResponse: httpResponse) 149 | let data = Data(bytes: [0,1,1,2,3,5,8]) 150 | expectedPyroEventSourceError = PyroEventSourceError.forcedClose 151 | eventSource = PyroEventSource.create(baseURL: baseURL, accessToken: accessToken) 152 | eventSource.state = .open 153 | eventSource.callback = self 154 | eventSource.urlSession(session, dataTask: task, didReceive: data) 155 | } 156 | 157 | func testURLSessionDidReceiveDataWithErroneousHTTPURLResponse() { 158 | let baseURL = "https://foo.firebaseio.com" 159 | let accessToken = "accessToken" 160 | let session = URLSession() 161 | let httpResponse = HTTPURLResponse(url: URL(string: "https://sampleio.com")!, statusCode: 500, httpVersion: nil, headerFields: nil) 162 | let task = URLSessionDataTaskMock(httpResponse: httpResponse) 163 | let data = Data(bytes: [0,1,1,2,3,5,8]) 164 | expectedRequestError = RequestError.internalServiceError("") 165 | eventSource = PyroEventSource.create(baseURL: baseURL, accessToken: accessToken) 166 | eventSource.state = .open 167 | eventSource.callback = self 168 | eventSource.urlSession(session, dataTask: task, didReceive: data) 169 | } 170 | 171 | func testURLSessionDidReceiveDataShouldCallOnReceiveMessage() { 172 | let baseURL = "https://foo.firebaseio.com" 173 | let accessToken = "accessToken" 174 | let session = URLSession() 175 | let httpResponse = HTTPURLResponse(url: URL(string: "https://sampleio.com")!, statusCode: 200, httpVersion: nil, headerFields: nil) 176 | let task = URLSessionDataTaskMock(httpResponse: httpResponse) 177 | let string = "id: 1\nevent: put\ndata: hello world\n\n" 178 | let data = string.data(using: .utf8)! 179 | expectedMessage = PyroEventSourceMessage() 180 | expectedMessage?.id = "1" 181 | expectedMessage?.event = "put" 182 | expectedMessage?.data = "hello world" 183 | eventSource = PyroEventSource.create(baseURL: baseURL, accessToken: accessToken) 184 | eventSource.state = .open 185 | eventSource.callback = self 186 | eventSource.urlSession(session, dataTask: task, didReceive: data) 187 | XCTAssertEqual(eventSource.lastEventID, expectedMessage!.id) 188 | } 189 | 190 | func testURLSessionDidReceiveRepsonseWithErroneousHTTPURLResponse() { 191 | let baseURL = "https://foo.firebaseio.com" 192 | let accessToken = "accessToken" 193 | let session = URLSession() 194 | let httpResponse = HTTPURLResponse(url: URL(string: "https://sampleio.com")!, statusCode: 500, httpVersion: nil, headerFields: nil) 195 | let task = URLSessionDataTaskMock(httpResponse: httpResponse) 196 | let completion: (URLSession.ResponseDisposition) -> Void = { _ in } 197 | expectedRequestError = RequestError.internalServiceError("") 198 | eventSource = PyroEventSource.create(baseURL: baseURL, accessToken: accessToken) 199 | eventSource.state = .open 200 | eventSource.callback = self 201 | eventSource.urlSession(session, dataTask: task, didReceive: httpResponse!, completionHandler: completion) 202 | } 203 | 204 | func testURLSessionDidReceiveResponseWithOpenState() { 205 | let baseURL = "https://foo.firebaseio.com" 206 | let accessToken = "accessToken" 207 | let session = URLSession() 208 | let httpResponse = HTTPURLResponse(url: URL(string: "https://sampleio.com")!, statusCode: 200, httpVersion: nil, headerFields: nil) 209 | let task = URLSessionDataTaskMock(httpResponse: httpResponse) 210 | let expectation1 = expectation(description: "testURLSessionDidReceiveResponse") 211 | let completion: (URLSession.ResponseDisposition) -> Void = { disposition in 212 | XCTAssertTrue(disposition == .allow) 213 | XCTAssertTrue(self.eventSource.state == .open) 214 | expectation1.fulfill() 215 | } 216 | expectedRequestError = RequestError.internalServiceError("") 217 | eventSource = PyroEventSource.create(baseURL: baseURL, accessToken: accessToken) 218 | eventSource.state = .open 219 | eventSource.callback = self 220 | eventSource.urlSession(session, dataTask: task, didReceive: httpResponse!, completionHandler: completion) 221 | waitForExpectations(timeout: 10) 222 | } 223 | 224 | func testURLSessionDidCompleteWithForcedCloseError() { 225 | let baseURL = "https://foo.firebaseio.com" 226 | let accessToken = "accessToken" 227 | let session = URLSession() 228 | let httpResponse = HTTPURLResponse(url: URL(string: "https://sampleio.com")!, statusCode: 204, httpVersion: nil, headerFields: nil) 229 | let task = URLSessionDataTaskMock(httpResponse: httpResponse) 230 | expectedPyroEventSourceError = PyroEventSourceError.forcedClose 231 | eventSource = PyroEventSource.create(baseURL: baseURL, accessToken: accessToken) 232 | eventSource.state = .open 233 | eventSource.callback = self 234 | eventSource.urlSession(session, task: task, didCompleteWithError: nil) 235 | } 236 | 237 | func testURLSessionDidCompleteWithNilError() { 238 | let baseURL = "https://foo.firebaseio.com" 239 | let accessToken = "accessToken" 240 | let session = URLSession() 241 | let httpResponse = HTTPURLResponse(url: URL(string: "https://sampleio.com")!, statusCode: 200, httpVersion: nil, headerFields: nil) 242 | let task = URLSessionDataTaskMock(httpResponse: httpResponse) 243 | eventSource = PyroEventSource.create(baseURL: baseURL, accessToken: accessToken) 244 | eventSource.state = .open 245 | eventSource.callback = self 246 | eventSource.urlSession(session, task: task, didCompleteWithError: nil) 247 | } 248 | 249 | func testURLSessionDidCompleteWithErroneousHTTPURLResponse() { 250 | let baseURL = "https://foo.firebaseio.com" 251 | let accessToken = "accessToken" 252 | let session = URLSession() 253 | let httpResponse = HTTPURLResponse(url: URL(string: "https://sampleio.com")!, statusCode: 500, httpVersion: nil, headerFields: nil) 254 | let task = URLSessionDataTaskMock(httpResponse: httpResponse) 255 | expectedRequestError = RequestError.internalServiceError("") 256 | eventSource = PyroEventSource.create(baseURL: baseURL, accessToken: accessToken) 257 | eventSource.state = .open 258 | eventSource.callback = self 259 | eventSource.urlSession(session, task: task, didCompleteWithError: nil) 260 | } 261 | } 262 | 263 | extension PyroEventSourceTest: PyroEventSourceCallback { 264 | 265 | func pyroEventSource(_ eventSource: PyroEventSource, didReceiveError error: Error) { 266 | XCTAssertTrue(eventSource == self.eventSource) 267 | 268 | if expectedPyroEventSourceError != nil { 269 | XCTAssertTrue(error is PyroEventSourceError) 270 | let eventSourceError = error as! PyroEventSourceError 271 | XCTAssertTrue(expectedPyroEventSourceError! == eventSourceError) 272 | 273 | switch eventSourceError { 274 | case .forcedClose: 275 | XCTAssertTrue(eventSource.state == .closed) 276 | XCTAssertNil(eventSource.session) 277 | default: 278 | break 279 | } 280 | 281 | } else if expectedRequestError != nil { 282 | XCTAssertTrue(error is RequestError) 283 | let requestError = error as! RequestError 284 | XCTAssertTrue(expectedRequestError! == requestError) 285 | XCTAssertTrue(eventSource.state == .closed) 286 | XCTAssertNil(eventSource.session) 287 | } 288 | } 289 | 290 | func pyroEventSource(_ eventSource: PyroEventSource, didReceiveMessage message: PyroEventSourceMessage) { 291 | XCTAssertTrue(eventSource == self.eventSource) 292 | 293 | if expectedMessage != nil { 294 | XCTAssertEqual(expectedMessage!.id, message.id) 295 | XCTAssertEqual(expectedMessage!.event, message.event) 296 | XCTAssertEqual(expectedMessage!.data, message.data) 297 | } 298 | } 299 | 300 | func pyroEventSourceOnOpen(_ eventSource: PyroEventSource) { 301 | XCTAssertTrue(eventSource == self.eventSource) 302 | XCTAssertTrue(eventSource.state == .open) 303 | 304 | isCalledOpenCallback = true 305 | } 306 | 307 | func pyroEventSourceOnClosed(_ eventSource: PyroEventSource) { 308 | XCTAssertTrue(eventSource == self.eventSource) 309 | XCTAssertTrue(eventSource.state == .closed) 310 | 311 | isCalledClosedCallback = true 312 | } 313 | 314 | func pyroEventSourceOnConnecting(_ eventSource: PyroEventSource) { 315 | XCTAssertTrue(eventSource == self.eventSource) 316 | XCTAssertTrue(eventSource.state == .connecting) 317 | 318 | isCalledConnectingCallback = true 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /PyrobaseTests/PyroTransactionElapsedTimeMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PyroTransactionElapsedTimeMock.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 20/06/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | @testable import Pyrobase 10 | 11 | class PyroTransactionElapsedTimeMock: PyroTransactionElapsedTimeProtocol { 12 | 13 | var startDate: Date = Date() 14 | var nowDate: Date = Date() 15 | 16 | var expectedSeconds: Int? = 0 17 | 18 | func seconds(from transactionDate: Date, to now: Date) -> Int? { 19 | startDate = transactionDate 20 | nowDate = now 21 | return expectedSeconds 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /PyrobaseTests/PyroTransactionMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PyroTransactionMock.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 20/06/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | @testable import Pyrobase 10 | 11 | class PyroTransactionMock: PyroTransaction { 12 | 13 | var shouldProceedWriteTransaction: Bool = true 14 | var shouldProceedReadChild: Bool = true 15 | var shouldProceedWriteChild: Bool = true 16 | var isDeleted: Bool = false 17 | 18 | var expectedSucceededInfo: Any? 19 | 20 | override func writeTransaction(param: Parameter) { 21 | guard !shouldProceedWriteTransaction else { 22 | super.writeTransaction(param: param) 23 | return 24 | } 25 | 26 | invokeCompletion(param) 27 | } 28 | 29 | override func readChild(param: Parameter) { 30 | guard !shouldProceedReadChild else { 31 | super.readChild(param: param) 32 | return 33 | } 34 | 35 | invokeCompletion(param) 36 | } 37 | 38 | override func writeChild(param: Parameter, info: Any) { 39 | guard !shouldProceedWriteChild else { 40 | super.writeChild(param: param, info: info) 41 | return 42 | } 43 | 44 | invokeCompletion(param) 45 | } 46 | 47 | override func deleteTransaction(param: Parameter, completion: @escaping (RequestResult) -> Void) { 48 | isDeleted = true 49 | completion(.succeeded(true)) 50 | } 51 | 52 | func invokeCompletion(_ param: Parameter) { 53 | guard expectedSucceededInfo != nil else { 54 | param.completion(.succeeded(true)) 55 | return 56 | } 57 | 58 | param.completion(.succeeded(expectedSucceededInfo!)) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /PyrobaseTests/PyroTransactionTemporaryPathMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PyroTransactionTemporaryPathMock.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 19/06/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | @testable import Pyrobase 10 | 11 | class PyroTransactionTemporaryPathMock: PyroTransactionTemporaryPathProtocol { 12 | 13 | var key: String = "" 14 | var expiration: UInt = 0 15 | var isExpired: Bool = false 16 | 17 | func isTransactionDateExpired(_ timestamp: Double = 0, now: Date) -> Bool { 18 | return isExpired 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /PyrobaseTests/PyroTransactionTemporaryPathTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PyroTransactionTemporaryPathTest.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 20/06/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Pyrobase 11 | 12 | class PyroTransactionTemporaryPathTest: XCTestCase { 13 | 14 | func testCreate() { 15 | let tempPath = PyroTransactionTemporaryPath.create() 16 | XCTAssertEqual(tempPath.expiration, 30) 17 | XCTAssertEqual(tempPath.key, "pyrobase_transactions") 18 | XCTAssertTrue(tempPath.elapsedTime is Calendar) 19 | XCTAssertEqual(tempPath.elapsedTime as! Calendar, Calendar.current) 20 | } 21 | 22 | func testIsTransactionDateExpiredWithCorrectTimestamp() { 23 | let elapsedTime = PyroTransactionElapsedTimeMock() 24 | let expiration: UInt = 10 25 | let key: String = "key" 26 | let now = Date() 27 | let tempPath = PyroTransactionTemporaryPath(key: key, expiration: expiration, elapsedTime: elapsedTime) 28 | let _ = tempPath.isTransactionDateExpired(1000, now: now) 29 | XCTAssertEqual(elapsedTime.startDate.timeIntervalSince1970, 1) 30 | } 31 | 32 | func testIsTransactionDateExpiredShouldReturnTrue() { 33 | let elapsedTime = PyroTransactionElapsedTimeMock() 34 | let expiration: UInt = 10 35 | let key: String = "key" 36 | let now = Date() 37 | 38 | var tempPath = PyroTransactionTemporaryPath(key: key, expiration: expiration, elapsedTime: elapsedTime) 39 | 40 | elapsedTime.expectedSeconds = 10 41 | var isExpired = tempPath.isTransactionDateExpired(1000, now: now) 42 | XCTAssertTrue(isExpired) 43 | 44 | elapsedTime.expectedSeconds = 11 45 | isExpired = tempPath.isTransactionDateExpired(1000, now: now) 46 | XCTAssertTrue(isExpired) 47 | 48 | tempPath = PyroTransactionTemporaryPath.create() 49 | 50 | var components = DateComponents() 51 | components.second = Int(tempPath.expiration) 52 | var futureDate = Calendar.current.date(byAdding: components, to: now)! 53 | isExpired = tempPath.isTransactionDateExpired(futureDate.timeIntervalSince1970 * 1000, now: now) 54 | XCTAssertTrue(isExpired) 55 | 56 | components.second = Int(tempPath.expiration + 1) 57 | futureDate = Calendar.current.date(byAdding: components, to: now)! 58 | isExpired = tempPath.isTransactionDateExpired(futureDate.timeIntervalSince1970 * 1000, now: now) 59 | XCTAssertTrue(isExpired) 60 | } 61 | 62 | func testIsTransactionDateExpiredShouldReturnFalse() { 63 | let elapsedTime = PyroTransactionElapsedTimeMock() 64 | let expiration: UInt = 10 65 | let key: String = "key" 66 | let now = Date() 67 | 68 | var tempPath = PyroTransactionTemporaryPath(key: key, expiration: expiration, elapsedTime: elapsedTime) 69 | 70 | elapsedTime.expectedSeconds = 9 71 | var isExpired = tempPath.isTransactionDateExpired(1000, now: now) 72 | XCTAssertFalse(isExpired) 73 | 74 | elapsedTime.expectedSeconds = 0 75 | isExpired = tempPath.isTransactionDateExpired(1000, now: now) 76 | XCTAssertFalse(isExpired) 77 | 78 | tempPath = PyroTransactionTemporaryPath.create() 79 | 80 | var components = DateComponents() 81 | components.second = Int(tempPath.expiration) - 1 82 | var futureDate = Calendar.current.date(byAdding: components, to: now)! 83 | isExpired = tempPath.isTransactionDateExpired(futureDate.timeIntervalSince1970 * 1000, now: now) 84 | XCTAssertFalse(isExpired) 85 | 86 | components.second = 0 87 | futureDate = Calendar.current.date(byAdding: components, to: now)! 88 | isExpired = tempPath.isTransactionDateExpired(futureDate.timeIntervalSince1970 * 1000, now: now) 89 | XCTAssertFalse(isExpired) 90 | } 91 | 92 | func testIsTransactionDateExpiredWithNilElapsedSeconds() { 93 | let elapsedTime = PyroTransactionElapsedTimeMock() 94 | let expiration: UInt = 10 95 | let key: String = "key" 96 | let now = Date() 97 | let tempPath = PyroTransactionTemporaryPath(key: key, expiration: expiration, elapsedTime: elapsedTime) 98 | 99 | elapsedTime.expectedSeconds = nil 100 | let isExpired = tempPath.isTransactionDateExpired(1000, now: now) 101 | XCTAssertFalse(isExpired) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /PyrobaseTests/PyroTransactionTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PyroTransactionTest.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 15/06/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Pyrobase 11 | 12 | class PyroTransactionTest: XCTestCase { 13 | 14 | func testCreate() { 15 | let baseURL = "https://foo.firebaseio.com" 16 | let accessToken = "accessToken" 17 | let transaction = PyroTransaction.create(baseURL: baseURL, accessToken: accessToken) 18 | XCTAssertTrue(transaction.tempPath is PyroTransactionTemporaryPath) 19 | XCTAssertEqual(transaction.tempPath.key, "pyrobase_transactions") 20 | XCTAssertEqual(transaction.tempPath.expiration, 30) 21 | XCTAssertEqual(transaction.baseURL, baseURL) 22 | XCTAssertTrue(transaction.path is RequestPath) 23 | XCTAssertTrue(transaction.request is Request) 24 | XCTAssertEqual((transaction.path as! RequestPath).accessToken, accessToken) 25 | XCTAssertEqual((transaction.path as! RequestPath).baseURL, baseURL) 26 | } 27 | 28 | func testReadTransactionWithCorrectPath() { 29 | let baseURL = "https://foo.firebaseio.com" 30 | let accessToken = "accessToken" 31 | let request = RequestMock() 32 | let path = RequestPath(baseURL: baseURL, accessToken: accessToken) 33 | let tempPath = PyroTransactionTemporaryPath.create() 34 | let transaction = PyroTransaction(request: request, path: path, tempPath: tempPath) 35 | let parentPath = "posts/abcde12345qwert" 36 | let childKey = "likes_count" 37 | let mutator: (Any) -> Any = { info in 38 | let count = info as! Int 39 | return count + 1 40 | } 41 | let expectation1 = expectation(description: "testReadTransaction") 42 | 43 | transaction.run(parentPath: parentPath, childKey: childKey, mutator: mutator) { _ in 44 | XCTAssertEqual(request.urlPath, "\(baseURL)/\(tempPath.key)/\(parentPath)/\(childKey).json?auth=\(accessToken)") 45 | expectation1.fulfill() 46 | } 47 | 48 | waitForExpectations(timeout: 10) 49 | } 50 | 51 | func testReadTransactionWithCustomError() { 52 | let baseURL = "https://foo.firebaseio.com" 53 | let accessToken = "accessToken" 54 | let request = RequestMock() 55 | let path = RequestPath(baseURL: baseURL, accessToken: accessToken) 56 | let tempPath = PyroTransactionTemporaryPath.create() 57 | let transaction = PyroTransaction(request: request, path: path, tempPath: tempPath) 58 | let parentPath = "posts/abcde12345qwert" 59 | let childKey = "likes_count" 60 | let mutator: (Any) -> Any = { info in 61 | let count = info as! Int 62 | return count + 1 63 | } 64 | let expectation1 = expectation(description: "testReadTransaction") 65 | 66 | request.expectedErrors.append(RequestError.invalidURL) 67 | 68 | transaction.run(parentPath: parentPath, childKey: childKey, mutator: mutator) { result in 69 | switch result { 70 | case .succeeded: 71 | XCTFail() 72 | 73 | case .failed(let info): 74 | XCTAssertTrue(info is RequestError) 75 | let errorInfo = info as! RequestError 76 | XCTAssertTrue(errorInfo == .invalidURL) 77 | } 78 | expectation1.fulfill() 79 | } 80 | 81 | waitForExpectations(timeout: 10) 82 | } 83 | 84 | func testReadTransactionWithInvalidExpirationTimestampError() { 85 | let baseURL = "https://foo.firebaseio.com" 86 | let accessToken = "accessToken" 87 | let request = RequestMock() 88 | let path = RequestPath(baseURL: baseURL, accessToken: accessToken) 89 | let tempPath = PyroTransactionTemporaryPathMock() 90 | let transaction = PyroTransaction(request: request, path: path, tempPath: tempPath) 91 | let parentPath = "posts/abcde12345qwert" 92 | let childKey = "likes_count" 93 | let mutator: (Any) -> Any = { info in 94 | let count = info as! Int 95 | return count + 1 96 | } 97 | let expectation1 = expectation(description: "testReadTransaction") 98 | 99 | request.expectedData.append("abcde12345qwert") 100 | 101 | transaction.run(parentPath: parentPath, childKey: childKey, mutator: mutator) { result in 102 | switch result { 103 | case .succeeded: 104 | XCTFail() 105 | 106 | case .failed(let info): 107 | XCTAssertTrue(info is PyroTransactionError) 108 | let errorInfo = info as! PyroTransactionError 109 | XCTAssertTrue(errorInfo == .invalidExpirationTimestamp) 110 | } 111 | expectation1.fulfill() 112 | } 113 | 114 | waitForExpectations(timeout: 10) 115 | } 116 | 117 | func testReadTransactionWithActiveTransactionNotDoneError() { 118 | let baseURL = "https://foo.firebaseio.com" 119 | let accessToken = "accessToken" 120 | let request = RequestMock() 121 | let path = RequestPath(baseURL: baseURL, accessToken: accessToken) 122 | let tempPath = PyroTransactionTemporaryPathMock() 123 | let transaction = PyroTransaction(request: request, path: path, tempPath: tempPath) 124 | let parentPath = "posts/abcde12345qwert" 125 | let childKey = "likes_count" 126 | let mutator: (Any) -> Any = { info in 127 | let count = info as! Int 128 | return count + 1 129 | } 130 | let expectation1 = expectation(description: "testReadTransaction") 131 | 132 | request.expectedData.append("1234567890") 133 | 134 | transaction.run(parentPath: parentPath, childKey: childKey, mutator: mutator) { result in 135 | switch result { 136 | case .succeeded: 137 | XCTFail() 138 | 139 | case .failed(let info): 140 | XCTAssertTrue(info is PyroTransactionError) 141 | let errorInfo = info as! PyroTransactionError 142 | XCTAssertTrue(errorInfo == .activeTransactionNotDone) 143 | } 144 | expectation1.fulfill() 145 | } 146 | 147 | waitForExpectations(timeout: 10) 148 | } 149 | 150 | func testWriteTransactionWithCorrectRequestInfo() { 151 | let baseURL = "https://foo.firebaseio.com" 152 | let accessToken = "accessToken" 153 | let request = RequestMock() 154 | let path = RequestPath(baseURL: baseURL, accessToken: accessToken) 155 | let tempPath = PyroTransactionTemporaryPath.create() 156 | let transaction = PyroTransaction(request: request, path: path, tempPath: tempPath) 157 | let parentPath = "posts/abcde12345qwert" 158 | let childKey = "likes_count" 159 | let mutator: (Any) -> Any = { info in 160 | let count = info as! Int 161 | return count + 1 162 | } 163 | let expectation1 = expectation(description: "testWriteTransaction") 164 | 165 | request.expectedErrors.append(RequestError.nullJSON) 166 | request.expectedErrors.append(RequestError.invalidURL) 167 | 168 | transaction.run(parentPath: parentPath, childKey: childKey, mutator: mutator) { _ in 169 | XCTAssertEqual(request.urlPath, "\(baseURL)/\(tempPath.key).json?auth=\(accessToken)") 170 | XCTAssertEqual(request.writeData.count, 1) 171 | 172 | let key = "\(parentPath)/\(childKey)" 173 | XCTAssertNotNil(request.writeData[key]) 174 | 175 | let value = request.writeData[key] as! [String: String] 176 | XCTAssertNotNil(value[".sv"]) 177 | XCTAssertEqual(value[".sv"], "timestamp") 178 | 179 | XCTAssertTrue(request.requestMethod == .patch) 180 | expectation1.fulfill() 181 | } 182 | 183 | waitForExpectations(timeout: 10) 184 | } 185 | 186 | func testWriteTransactionWithCustomError() { 187 | let baseURL = "https://foo.firebaseio.com" 188 | let accessToken = "accessToken" 189 | let request = RequestMock() 190 | let path = RequestPath(baseURL: baseURL, accessToken: accessToken) 191 | let tempPath = PyroTransactionTemporaryPath.create() 192 | let transaction = PyroTransaction(request: request, path: path, tempPath: tempPath) 193 | let parentPath = "posts/abcde12345qwert" 194 | let childKey = "likes_count" 195 | let mutator: (Any) -> Any = { info in 196 | let count = info as! Int 197 | return count + 1 198 | } 199 | let expectation1 = expectation(description: "testWriteTransaction") 200 | 201 | request.expectedErrors.append(RequestError.nullJSON) 202 | request.expectedErrors.append(RequestError.invalidURL) 203 | 204 | transaction.run(parentPath: parentPath, childKey: childKey, mutator: mutator) { result in 205 | switch result { 206 | case .succeeded: 207 | XCTFail() 208 | 209 | case .failed(let info): 210 | XCTAssertTrue(info is RequestError) 211 | let errorInfo = info as! RequestError 212 | XCTAssertTrue(errorInfo == .invalidURL) 213 | } 214 | 215 | expectation1.fulfill() 216 | } 217 | 218 | waitForExpectations(timeout: 10) 219 | } 220 | 221 | func testWriteTransactionShouldBeCalledWhenTransactionDateExpiredOnRead() { 222 | let baseURL = "https://foo.firebaseio.com" 223 | let accessToken = "accessToken" 224 | let request = RequestMock() 225 | let path = RequestPath(baseURL: baseURL, accessToken: accessToken) 226 | let tempPath = PyroTransactionTemporaryPathMock() 227 | let transaction = PyroTransactionMock(request: request, path: path, tempPath: tempPath) 228 | let parentPath = "posts/abcde12345qwert" 229 | let childKey = "likes_count" 230 | let mutator: (Any) -> Any = { info in 231 | let count = info as! Int 232 | return count + 1 233 | } 234 | let expectation1 = expectation(description: "testReadTransaction") 235 | 236 | tempPath.isExpired = true 237 | request.expectedData.append("1234567890") 238 | transaction.shouldProceedWriteTransaction = false 239 | 240 | transaction.run(parentPath: parentPath, childKey: childKey, mutator: mutator) { result in 241 | expectation1.fulfill() 242 | } 243 | 244 | waitForExpectations(timeout: 10) 245 | } 246 | 247 | func testReadChildWithCorrectRequestInfo() { 248 | let baseURL = "https://foo.firebaseio.com" 249 | let accessToken = "accessToken" 250 | let request = RequestMock() 251 | let path = RequestPath(baseURL: baseURL, accessToken: accessToken) 252 | let tempPath = PyroTransactionTemporaryPath.create() 253 | let transaction = PyroTransaction(request: request, path: path, tempPath: tempPath) 254 | let parentPath = "posts/abcde12345qwert" 255 | let childKey = "likes_count" 256 | let mutator: (Any) -> Any = { info in 257 | let count = info as! Int 258 | return count + 1 259 | } 260 | let expectation1 = expectation(description: "testReadChild") 261 | 262 | request.shouldURLPathBeReplaced = false 263 | request.shouldRequestMethodBeReplaced = false 264 | request.expectedErrors.append(RequestError.nullJSON) 265 | request.expectedErrors.append(nil) 266 | request.expectedErrors.append(RequestError.invalidURL) 267 | request.expectedData.append(true) 268 | 269 | transaction.run(parentPath: parentPath, childKey: childKey, mutator: mutator) { _ in 270 | XCTAssertEqual(request.urlPath, "\(baseURL)/\(parentPath)/\(childKey).json?auth=\(accessToken)") 271 | XCTAssertTrue(request.requestMethod == .get) 272 | expectation1.fulfill() 273 | } 274 | 275 | waitForExpectations(timeout: 10) 276 | } 277 | 278 | func testReadChildWithCustomError() { 279 | let baseURL = "https://foo.firebaseio.com" 280 | let accessToken = "accessToken" 281 | let request = RequestMock() 282 | let path = RequestPath(baseURL: baseURL, accessToken: accessToken) 283 | let tempPath = PyroTransactionTemporaryPath.create() 284 | let transaction = PyroTransactionMock(request: request, path: path, tempPath: tempPath) 285 | let parentPath = "posts/abcde12345qwert" 286 | let childKey = "likes_count" 287 | let mutator: (Any) -> Any = { info in 288 | let count = info as! Int 289 | return count + 1 290 | } 291 | let expectation1 = expectation(description: "testReadChild") 292 | 293 | request.expectedErrors.append(RequestError.nullJSON) 294 | request.expectedErrors.append(nil) 295 | request.expectedErrors.append(RequestError.invalidURL) 296 | request.expectedData.append(true) 297 | 298 | transaction.run(parentPath: parentPath, childKey: childKey, mutator: mutator) { result in 299 | switch result { 300 | case .succeeded: 301 | XCTFail() 302 | 303 | case .failed(let info): 304 | XCTAssertTrue(info is RequestError) 305 | let errorInfo = info as! RequestError 306 | XCTAssertTrue(errorInfo == .invalidURL) 307 | } 308 | 309 | XCTAssertTrue(transaction.isDeleted) 310 | expectation1.fulfill() 311 | } 312 | 313 | waitForExpectations(timeout: 10) 314 | } 315 | 316 | func testWriteChildShouldBeCalledWhenSucceededReadChild() { 317 | let baseURL = "https://foo.firebaseio.com" 318 | let accessToken = "accessToken" 319 | let request = RequestMock() 320 | let path = RequestPath(baseURL: baseURL, accessToken: accessToken) 321 | let tempPath = PyroTransactionTemporaryPathMock() 322 | let transaction = PyroTransactionMock(request: request, path: path, tempPath: tempPath) 323 | let parentPath = "posts/abcde12345qwert" 324 | let childKey = "likes_count" 325 | let mutator: (Any) -> Any = { info in 326 | let count = info as! Int 327 | return count + 1 328 | } 329 | let expectation1 = expectation(description: "testWriteChild") 330 | 331 | request.expectedErrors.append(RequestError.nullJSON) 332 | request.expectedErrors.append(nil) 333 | request.expectedErrors.append(nil) 334 | request.expectedData.append(true) 335 | request.expectedData.append(true) 336 | 337 | transaction.shouldProceedWriteChild = false 338 | 339 | transaction.run(parentPath: parentPath, childKey: childKey, mutator: mutator) { result in 340 | expectation1.fulfill() 341 | } 342 | 343 | waitForExpectations(timeout: 10) 344 | } 345 | 346 | func testWriteChildWithCorrectRequestInfo() { 347 | let baseURL = "https://foo.firebaseio.com" 348 | let accessToken = "accessToken" 349 | let request = RequestMock() 350 | let path = RequestPath(baseURL: baseURL, accessToken: accessToken) 351 | let tempPath = PyroTransactionTemporaryPath.create() 352 | let transaction = PyroTransaction(request: request, path: path, tempPath: tempPath) 353 | let parentPath = "posts/abcde12345qwert" 354 | let childKey = "likes_count" 355 | let mutator: (Any) -> Any = { info in 356 | let count = info as! Int 357 | return count + 1 358 | } 359 | let expectation1 = expectation(description: "testReadChild") 360 | 361 | request.shouldURLPathBeReplaced = false 362 | request.shouldRequestMethodBeReplaced = false 363 | request.expectedErrors.append(RequestError.nullJSON) 364 | request.expectedErrors.append(nil) 365 | request.expectedErrors.append(nil) 366 | request.expectedErrors.append(RequestError.invalidURL) 367 | request.expectedData.append(true) 368 | request.expectedData.append(1) 369 | 370 | transaction.run(parentPath: parentPath, childKey: childKey, mutator: mutator) { _ in 371 | XCTAssertEqual(request.urlPath, "\(baseURL)/\(parentPath).json?auth=\(accessToken)") 372 | XCTAssertEqual(request.writeData.count, 1) 373 | 374 | let key = "\(childKey)" 375 | let expectedData: Int = mutator(1) as! Int 376 | 377 | XCTAssertNotNil(request.writeData[key]) 378 | XCTAssertEqual(request.writeData[key] as! Int, expectedData) 379 | XCTAssertTrue(request.requestMethod == .patch) 380 | expectation1.fulfill() 381 | } 382 | 383 | waitForExpectations(timeout: 10) 384 | } 385 | 386 | func testWriteChildWithCustomError() { 387 | let baseURL = "https://foo.firebaseio.com" 388 | let accessToken = "accessToken" 389 | let request = RequestMock() 390 | let path = RequestPath(baseURL: baseURL, accessToken: accessToken) 391 | let tempPath = PyroTransactionTemporaryPath.create() 392 | let transaction = PyroTransactionMock(request: request, path: path, tempPath: tempPath) 393 | let parentPath = "posts/abcde12345qwert" 394 | let childKey = "likes_count" 395 | let mutator: (Any) -> Any = { info in 396 | let count = info as! Int 397 | return count + 1 398 | } 399 | let expectation1 = expectation(description: "testReadChild") 400 | 401 | request.expectedErrors.append(RequestError.nullJSON) 402 | request.expectedErrors.append(nil) 403 | request.expectedErrors.append(nil) 404 | request.expectedErrors.append(RequestError.invalidURL) 405 | request.expectedData.append(true) 406 | request.expectedData.append(1) 407 | 408 | transaction.run(parentPath: parentPath, childKey: childKey, mutator: mutator) { result in 409 | switch result { 410 | case .succeeded: 411 | XCTFail() 412 | 413 | case .failed(let info): 414 | XCTAssertTrue(info is RequestError) 415 | let errorInfo = info as! RequestError 416 | XCTAssertTrue(errorInfo == .invalidURL) 417 | } 418 | 419 | XCTAssertTrue(transaction.isDeleted) 420 | expectation1.fulfill() 421 | } 422 | 423 | waitForExpectations(timeout: 10) 424 | } 425 | 426 | func testWriteChildWithSucceededResult() { 427 | let baseURL = "https://foo.firebaseio.com" 428 | let accessToken = "accessToken" 429 | let request = RequestMock() 430 | let path = RequestPath(baseURL: baseURL, accessToken: accessToken) 431 | let tempPath = PyroTransactionTemporaryPath.create() 432 | let transaction = PyroTransactionMock(request: request, path: path, tempPath: tempPath) 433 | let parentPath = "posts/abcde12345qwert" 434 | let childKey = "likes_count" 435 | let mutator: (Any) -> Any = { info in 436 | let count = info as! Int 437 | return count + 1 438 | } 439 | let expectation1 = expectation(description: "testReadChild") 440 | 441 | request.expectedErrors.append(RequestError.nullJSON) 442 | request.expectedErrors.append(nil) 443 | request.expectedErrors.append(nil) 444 | request.expectedErrors.append(nil) 445 | request.expectedData.append(true) 446 | request.expectedData.append(1) 447 | request.expectedData.append(["likes_count": mutator(1)]) 448 | 449 | transaction.run(parentPath: parentPath, childKey: childKey, mutator: mutator) { result in 450 | switch result { 451 | case .failed: 452 | XCTFail() 453 | 454 | case .succeeded(let info): 455 | XCTAssertTrue(info is [AnyHashable: Any]) 456 | let resultInfo = info as! [AnyHashable: Any] 457 | let likesCount = resultInfo["likes_count"] 458 | XCTAssertTrue(likesCount is Int) 459 | XCTAssertEqual(likesCount as! Int, 2) 460 | } 461 | 462 | XCTAssertTrue(transaction.isDeleted) 463 | expectation1.fulfill() 464 | } 465 | 466 | waitForExpectations(timeout: 10) 467 | } 468 | 469 | func testDeleteTransactionWithCorrectRequestInfo() { 470 | let baseURL = "https://foo.firebaseio.com" 471 | let accessToken = "accessToken" 472 | let request = RequestMock() 473 | let path = RequestPath(baseURL: baseURL, accessToken: accessToken) 474 | let tempPath = PyroTransactionTemporaryPath.create() 475 | let transaction = PyroTransaction(request: request, path: path, tempPath: tempPath) 476 | let parentPath = "posts/abcde12345qwert" 477 | let childKey = "likes_count" 478 | let mutator: (Any) -> Any = { info in 479 | let count = info as! Int 480 | return count + 1 481 | } 482 | let expectation1 = expectation(description: "testDeleteTransaction") 483 | 484 | request.shouldURLPathBeReplaced = true 485 | request.shouldRequestMethodBeReplaced = true 486 | request.expectedErrors.append(RequestError.nullJSON) 487 | request.expectedErrors.append(nil) 488 | request.expectedErrors.append(nil) 489 | request.expectedErrors.append(RequestError.invalidURL) 490 | request.expectedData.append(true) 491 | request.expectedData.append(1) 492 | request.expectedData.append(true) 493 | 494 | transaction.run(parentPath: parentPath, childKey: childKey, mutator: mutator) { result in 495 | XCTAssertEqual(request.urlPath, "\(baseURL)/\(tempPath.key)/\(parentPath).json?auth=\(accessToken)") 496 | XCTAssertTrue(request.requestMethod == .delete) 497 | expectation1.fulfill() 498 | } 499 | 500 | waitForExpectations(timeout: 10) 501 | } 502 | } 503 | -------------------------------------------------------------------------------- /PyrobaseTests/PyrobaseTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PyrobaseTest.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 01/05/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Pyrobase 11 | 12 | class PyrobaseTest: XCTestCase { 13 | 14 | func testInitialization() { 15 | let baseURL = "https://foo.firebaseio.com" 16 | let accessToken = "accessToken" 17 | let path = RequestPath(baseURL: baseURL, accessToken: accessToken) 18 | let request = Request.create() 19 | let pyrobase = Pyrobase(request: request, path: path) 20 | 21 | XCTAssertNotNil(pyrobase.path) 22 | XCTAssertNotNil(pyrobase.request) 23 | } 24 | 25 | func testCreate() { 26 | let baseURL = "https://foo.firebaseio.com" 27 | let accessToken = "accessToken" 28 | let pyrobase = Pyrobase.create(baseURL: baseURL, accessToken: accessToken) 29 | 30 | XCTAssertNotNil(pyrobase.path) 31 | XCTAssertTrue(pyrobase.path is RequestPath) 32 | } 33 | 34 | func testGet() { 35 | let baseURL = "https://foo.firebaseio.com" 36 | let accessToken = "accessToken" 37 | let path = RequestPath(baseURL: baseURL, accessToken: accessToken) 38 | let request = RequestMock() 39 | let pyrobase = Pyrobase(request: request, path: path) 40 | 41 | let expectation1 = expectation(description: "testGet") 42 | pyrobase.get(path: "name", query: [:]) { result in 43 | switch result { 44 | case .failed: 45 | XCTFail() 46 | 47 | case .succeeded(let data): 48 | XCTAssertTrue(data is String) 49 | XCTAssertEqual(data as! String, "Luche") 50 | } 51 | expectation1.fulfill() 52 | } 53 | 54 | waitForExpectations(timeout: 1) 55 | } 56 | 57 | func testPut() { 58 | let baseURL = "https://foo.firebaseio.com" 59 | let accessToken = "accessToken" 60 | let path = RequestPath(baseURL: baseURL, accessToken: accessToken) 61 | let request = RequestMock() 62 | let pyrobase = Pyrobase(request: request, path: path) 63 | let expectedValue = ["last_name": "Valdez"] 64 | let expectation1 = expectation(description: "testPut") 65 | 66 | pyrobase.put(path: "name", value: expectedValue) { result in 67 | switch result { 68 | case .failed: 69 | XCTFail() 70 | 71 | case .succeeded(let data): 72 | XCTAssertTrue(data is [String: String]) 73 | let resultInfo = data as! [String: String] 74 | XCTAssertEqual(resultInfo.count, expectedValue.count) 75 | XCTAssertEqual(resultInfo["last_name"], expectedValue["last_name"]) 76 | 77 | let absolutePath = path.build("name") 78 | let content = request.content[absolutePath] 79 | XCTAssertTrue(content is [String: String]) 80 | let contentInfo = content as! [String: String] 81 | XCTAssertEqual(contentInfo.count, expectedValue.count) 82 | XCTAssertEqual(contentInfo["last_name"], expectedValue["last_name"]) 83 | } 84 | expectation1.fulfill() 85 | } 86 | 87 | waitForExpectations(timeout: 1) 88 | } 89 | 90 | func testPost() { 91 | let baseURL = "https://foo.firebaseio.com" 92 | let accessToken = "accessToken" 93 | let path = RequestPath(baseURL: baseURL, accessToken: accessToken) 94 | let request = RequestMock() 95 | let pyrobase = Pyrobase(request: request, path: path) 96 | let expectedValue = ["message": "Hello world!"] 97 | let expectation1 = expectation(description: "testPost") 98 | 99 | pyrobase.post(path: "messages", value: expectedValue) { result in 100 | switch result { 101 | case .failed: 102 | XCTFail() 103 | 104 | case .succeeded(let data): 105 | XCTAssertTrue(data is [String: String]) 106 | let resultInfo = data as! [String: String] 107 | XCTAssertEqual(resultInfo.count, 1) 108 | XCTAssertNotNil(resultInfo["name"]) 109 | 110 | let content = request.content[resultInfo["name"]!] 111 | XCTAssertTrue(content is [String: String]) 112 | let contentInfo = content as! [String: String] 113 | XCTAssertEqual(contentInfo.count, expectedValue.count) 114 | XCTAssertEqual(contentInfo["message"], expectedValue["message"]) 115 | } 116 | expectation1.fulfill() 117 | } 118 | 119 | waitForExpectations(timeout: 1) 120 | } 121 | 122 | func testPatch() { 123 | let baseURL = "https://foo.firebaseio.com" 124 | let accessToken = "accessToken" 125 | let path = RequestPath(baseURL: baseURL, accessToken: accessToken) 126 | let request = RequestMock() 127 | let pyrobase = Pyrobase(request: request, path: path) 128 | let expectedValue = ["date": "June 8, 2017"] 129 | let expectation1 = expectation(description: "testPost") 130 | 131 | pyrobase.patch(path: "messages/abcde12345qwert", value: expectedValue) { result in 132 | switch result { 133 | case .failed: 134 | XCTFail() 135 | 136 | case .succeeded(let data): 137 | XCTAssertTrue(data is [String: String]) 138 | let resultInfo = data as! [String: String] 139 | XCTAssertEqual(resultInfo.count, expectedValue.count) 140 | XCTAssertEqual(resultInfo["date"], expectedValue["date"]) 141 | 142 | let absolutePath = path.build("messages/abcde12345qwert") 143 | let content = request.content[absolutePath] 144 | XCTAssertTrue(content is [String: String]) 145 | let contentInfo = content as! [String: String] 146 | XCTAssertEqual(contentInfo.count, 2) 147 | XCTAssertEqual(contentInfo["message"], "Hello World!") 148 | XCTAssertEqual(contentInfo["date"], expectedValue["date"]) 149 | } 150 | expectation1.fulfill() 151 | } 152 | 153 | waitForExpectations(timeout: 1) 154 | } 155 | 156 | func testDelete() { 157 | let baseURL = "https://foo.firebaseio.com" 158 | let accessToken = "accessToken" 159 | let path = RequestPath(baseURL: baseURL, accessToken: accessToken) 160 | let request = RequestMock() 161 | let pyrobase = Pyrobase(request: request, path: path) 162 | let expectation1 = expectation(description: "testDelete") 163 | 164 | pyrobase.delete(path: "name") { result in 165 | switch result { 166 | case .failed: 167 | XCTFail() 168 | 169 | case .succeeded(let info): 170 | XCTAssertTrue(info is String) 171 | let resultInfo = info as! String 172 | XCTAssertEqual(resultInfo.lowercased(), "null") 173 | 174 | let absolutePath = path.build("name") 175 | XCTAssertNil(request.content[absolutePath]) 176 | } 177 | expectation1.fulfill() 178 | } 179 | 180 | waitForExpectations(timeout: 1) 181 | } 182 | 183 | func testGetBaseURL() { 184 | let baseURL = "https://foo.firebaseio.com" 185 | let accessToken = "accessToken" 186 | let path = RequestPath(baseURL: baseURL, accessToken: accessToken) 187 | let request = RequestMock() 188 | let pyrobase = Pyrobase(request: request, path: path) 189 | 190 | XCTAssertEqual(pyrobase.baseURL, baseURL) 191 | } 192 | 193 | func testGetWithQuery() { 194 | let baseURL = "https://foo.firebaseio.com" 195 | let accessToken = "accessToken" 196 | let path = RequestPath(baseURL: baseURL, accessToken: accessToken) 197 | let session = URLSessionMock() 198 | let operation = JSONRequestOperation.create() 199 | let response = RequestResponse() 200 | let request = Request(session: session, operation: operation, response: response) 201 | let pyrobase = Pyrobase(request: request, path: path) 202 | let query: [AnyHashable: Any] = ["orderBy": "\"$key\"", "limitToFirst": 1] 203 | let expectation1 = expectation(description: "testGet") 204 | pyrobase.get(path: "name", query: query) { result in 205 | switch result { 206 | case .failed: 207 | XCTFail() 208 | 209 | case .succeeded: 210 | break 211 | } 212 | expectation1.fulfill() 213 | } 214 | 215 | waitForExpectations(timeout: 1) 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /PyrobaseTests/RequestErrorTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestErrorTest.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 26/07/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Pyrobase 11 | 12 | class RequestErrorTest: XCTestCase { 13 | 14 | func testCode() { 15 | XCTAssertEqual(RequestError.invalidURL.code, -9000) 16 | XCTAssertEqual(RequestError.unparseableJSON.code, -9001) 17 | XCTAssertEqual(RequestError.noURLResponse.code, -9002) 18 | XCTAssertEqual(RequestError.nullJSON.code, -9003) 19 | XCTAssertEqual(RequestError.unknown.code, -9004) 20 | 21 | XCTAssertEqual(RequestError.badRequest("").code, -9005) 22 | XCTAssertEqual(RequestError.unauthorized("").code, -9006) 23 | XCTAssertEqual(RequestError.forbidden("").code, -9007) 24 | XCTAssertEqual(RequestError.notFound("").code, -9008) 25 | XCTAssertEqual(RequestError.internalServiceError("").code, -9009) 26 | XCTAssertEqual(RequestError.serviceUnavailable("").code, -9010) 27 | } 28 | 29 | func testMessage() { 30 | XCTAssertEqual(RequestError.invalidURL.message, "URL is invalid") 31 | XCTAssertEqual(RequestError.unparseableJSON.message, "Can not parse JSON") 32 | XCTAssertEqual(RequestError.noURLResponse.message, "No URL response") 33 | XCTAssertEqual(RequestError.nullJSON.message, "JSON is null") 34 | XCTAssertEqual(RequestError.unknown.message, "Unknown error encountered") 35 | 36 | XCTAssertEqual(RequestError.badRequest("Bad request").message, "Bad request") 37 | XCTAssertEqual(RequestError.unauthorized("Unauthorized").message, "Unauthorized") 38 | XCTAssertEqual(RequestError.forbidden("Forbidden").message, "Forbidden") 39 | XCTAssertEqual(RequestError.notFound("Not Found").message, "Not Found") 40 | XCTAssertEqual(RequestError.internalServiceError("Internal Service Error").message, "Internal Service Error") 41 | XCTAssertEqual(RequestError.serviceUnavailable("Service Unavailable").message, "Service Unavailable") 42 | } 43 | 44 | func testEquality() { 45 | XCTAssertEqual(RequestError.invalidURL, RequestError.invalidURL) 46 | XCTAssertNotEqual(RequestError.invalidURL, RequestError.noURLResponse) 47 | XCTAssertNotEqual(RequestError.invalidURL, RequestError.badRequest("Bad request")) 48 | XCTAssertEqual(RequestError.badRequest("Bad request"), RequestError.badRequest("Bad request")) 49 | XCTAssertNotEqual(RequestError.badRequest("Bad request"), RequestError.badRequest("Worse request")) 50 | XCTAssertNotEqual(RequestError.badRequest("Bad request"), RequestError.unauthorized("Unauthorized")) 51 | } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /PyrobaseTests/RequestMethodTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestMethodTest.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 07/06/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Pyrobase 11 | 12 | class RequestMethodTest: XCTestCase { 13 | 14 | func testPrintedDescription() { 15 | var method: RequestMethod = .get 16 | XCTAssertEqual("\(method)", "GET") 17 | 18 | method = .post 19 | XCTAssertEqual("\(method)", "POST") 20 | 21 | method = .put 22 | XCTAssertEqual("\(method)", "PUT") 23 | 24 | method = .patch 25 | XCTAssertEqual("\(method)", "PATCH") 26 | 27 | method = .delete 28 | XCTAssertEqual("\(method)", "DELETE") 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /PyrobaseTests/RequestMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestMock.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 02/05/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | import Pyrobase 10 | 11 | class RequestMock: RequestProtocol { 12 | 13 | var content: [AnyHashable: Any] = [ 14 | "https://foo.firebaseio.com/name.json?auth=accessToken": "Luche", 15 | "https://foo.firebaseio.com/messages/abcde12345qwert.json?auth=accessToken": ["message": "Hello World!"] 16 | ] 17 | 18 | var urlPath: String = "" 19 | var expectedErrors: [Error?] = [] 20 | var expectedData: [Any] = [] 21 | var writeData: [AnyHashable : Any] = [:] 22 | var requestMethod: RequestMethod = .get 23 | var shouldURLPathBeReplaced: Bool = true 24 | var shouldRequestMethodBeReplaced: Bool = true 25 | 26 | func read(path: String, query: [AnyHashable: Any], completion: @escaping (RequestResult) -> Void) { 27 | urlPath = path 28 | requestMethod = .get 29 | 30 | if !expectedErrors.isEmpty, let error = expectedErrors.removeFirst() { 31 | completion(.failed(error)) 32 | 33 | } else { 34 | if !expectedData.isEmpty { 35 | completion(.succeeded(expectedData.removeFirst())) 36 | 37 | } else { 38 | if let item = content[path] { 39 | completion(.succeeded(item)) 40 | 41 | } else { 42 | completion(.succeeded("OK")) 43 | } 44 | } 45 | } 46 | } 47 | 48 | func write(path: String, method: RequestMethod, data: [AnyHashable : Any], completion: @escaping (RequestResult) -> Void) { 49 | urlPath = path 50 | writeData = data 51 | requestMethod = method 52 | 53 | switch method { 54 | case .put: 55 | content[path] = data 56 | completion(.succeeded(data)) 57 | 58 | case .post: 59 | let newId = "abcde12345qwert" 60 | content[newId] = data 61 | completion(.succeeded(["name": newId])) 62 | 63 | case .patch: 64 | if !expectedErrors.isEmpty, let error = expectedErrors.removeFirst() { 65 | completion(.failed(error)) 66 | 67 | } else { 68 | if !expectedData.isEmpty { 69 | completion(.succeeded(expectedData.removeFirst())) 70 | 71 | } else { 72 | var contentInfo = content[path] as! [AnyHashable: Any] 73 | for (key, value) in data { 74 | contentInfo[key] = value 75 | } 76 | content[path] = contentInfo 77 | completion(.succeeded(data)) 78 | } 79 | } 80 | 81 | default: 82 | break 83 | } 84 | } 85 | 86 | func delete(path: String, completion: @escaping (RequestResult) -> Void) { 87 | if shouldURLPathBeReplaced { 88 | urlPath = path 89 | } 90 | 91 | if shouldRequestMethodBeReplaced { 92 | requestMethod = .delete 93 | } 94 | 95 | content.removeValue(forKey: path) 96 | completion(.succeeded("null")) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /PyrobaseTests/RequestOperationMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestOperationMock.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 08/06/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | import Pyrobase 10 | 11 | class RequestOperationMock: RequestOperation { 12 | 13 | enum MockError: Error { 14 | 15 | case failedToParse 16 | } 17 | 18 | func build(url: URL, method: RequestMethod, data: [AnyHashable : Any]) -> URLRequest { 19 | return URLRequest(url: url) 20 | } 21 | 22 | func parse(data: Data) -> RequestOperationResult { 23 | return .error(MockError.failedToParse) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /PyrobaseTests/RequestPathTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestPathTest.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 02/05/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Pyrobase 11 | 12 | class RequestPathTest: XCTestCase { 13 | 14 | func testInitialization() { 15 | let accessToken = "accessToken" 16 | let baseURL = "https://foo.firebaseio.com" 17 | let requestPath = RequestPath(baseURL: baseURL, accessToken: accessToken) 18 | XCTAssertNotNil(requestPath.baseURL) 19 | XCTAssertNotNil(requestPath.accessToken) 20 | XCTAssertEqual(requestPath.baseURL, baseURL) 21 | XCTAssertEqual(requestPath.accessToken, accessToken) 22 | } 23 | 24 | func testBuild() { 25 | let accessToken = "accessToken" 26 | let baseURL = "https://foo.firebaseio.com" 27 | let relativePath = "foo" 28 | let requestPath = RequestPath(baseURL: baseURL, accessToken: accessToken) 29 | let requestURL = "\(baseURL)/\(relativePath).json?auth=\(accessToken)" 30 | XCTAssertEqual(requestURL, requestPath.build(relativePath)) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /PyrobaseTests/RequestResponseTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestResponseTest.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 23/06/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Pyrobase 11 | 12 | class RequestResponseTest: XCTestCase { 13 | 14 | func testIsErroneous() { 15 | let response = RequestResponse() 16 | 17 | var httpResponse = HTTPURLResponse(url: URL(string: "https://sampleio.com")!, statusCode: 400, httpVersion: nil, headerFields: nil)! 18 | var error = response.isErroneous(httpResponse, data: nil) 19 | XCTAssertNotNil(error) 20 | XCTAssertTrue(error as! RequestError == .badRequest("")) 21 | 22 | httpResponse = HTTPURLResponse(url: URL(string: "https://sampleio.com")!, statusCode: 401, httpVersion: nil, headerFields: nil)! 23 | error = response.isErroneous(httpResponse, data: nil) 24 | XCTAssertNotNil(error) 25 | XCTAssertTrue(error as! RequestError == .unauthorized("")) 26 | 27 | httpResponse = HTTPURLResponse(url: URL(string: "https://sampleio.com")!, statusCode: 403, httpVersion: nil, headerFields: nil)! 28 | error = response.isErroneous(httpResponse, data: nil) 29 | XCTAssertNotNil(error) 30 | XCTAssertTrue(error as! RequestError == .forbidden("")) 31 | 32 | httpResponse = HTTPURLResponse(url: URL(string: "https://sampleio.com")!, statusCode: 404, httpVersion: nil, headerFields: nil)! 33 | error = response.isErroneous(httpResponse, data: nil) 34 | XCTAssertNotNil(error) 35 | XCTAssertTrue(error as! RequestError == .notFound("")) 36 | 37 | httpResponse = HTTPURLResponse(url: URL(string: "https://sampleio.com")!, statusCode: 500, httpVersion: nil, headerFields: nil)! 38 | error = response.isErroneous(httpResponse, data: nil) 39 | XCTAssertNotNil(error) 40 | XCTAssertTrue(error as! RequestError == .internalServiceError("")) 41 | 42 | httpResponse = HTTPURLResponse(url: URL(string: "https://sampleio.com")!, statusCode: 503, httpVersion: nil, headerFields: nil)! 43 | error = response.isErroneous(httpResponse, data: nil) 44 | XCTAssertNotNil(error) 45 | XCTAssertTrue(error as! RequestError == .serviceUnavailable("")) 46 | 47 | httpResponse = HTTPURLResponse(url: URL(string: "https://sampleio.com")!, statusCode: 200, httpVersion: nil, headerFields: nil)! 48 | error = response.isErroneous(httpResponse, data: nil) 49 | XCTAssertNil(error) 50 | XCTAssertNil(response.isErroneous(nil, data: nil)) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /PyrobaseTests/RequestTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestTest.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 02/05/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Pyrobase 11 | 12 | class RequestTest: XCTestCase { 13 | 14 | func testRead() { 15 | let session = URLSessionMock() 16 | let operation = JSONRequestOperation.create() 17 | let response = RequestResponse() 18 | let request = Request(session: session, operation: operation, response: response) 19 | let expectation1 = expectation(description: "testRead") 20 | request.read(path: "https://foo.firebaseio.com/users/12345/name.json?access_token=accessToken", query: [:]) { result in 21 | switch result { 22 | case .failed: 23 | XCTFail() 24 | 25 | case .succeeded(let data): 26 | XCTAssertTrue(data is String) 27 | XCTAssertEqual(data as! String, "Luche") 28 | } 29 | expectation1.fulfill() 30 | } 31 | waitForExpectations(timeout: 1) 32 | } 33 | 34 | func testReadWithInt() { 35 | let session = URLSessionMock() 36 | let operation = JSONRequestOperation.create() 37 | let response = RequestResponse() 38 | let request = Request(session: session, operation: operation, response: response) 39 | let expectation1 = expectation(description: "testRead") 40 | request.read(path: "https://foo.firebaseio.com/users/12345/int.json?access_token=accessToken", query: [:]) { result in 41 | switch result { 42 | case .failed: 43 | XCTFail() 44 | 45 | case .succeeded(let data): 46 | XCTAssertTrue(data is String) 47 | let number = Int(data as! String) 48 | XCTAssertNotNil(number) 49 | XCTAssertEqual(number, 101) 50 | } 51 | expectation1.fulfill() 52 | } 53 | waitForExpectations(timeout: 1) 54 | } 55 | 56 | func testReadWithDouble() { 57 | let session = URLSessionMock() 58 | let operation = JSONRequestOperation.create() 59 | let response = RequestResponse() 60 | let request = Request(session: session, operation: operation, response: response) 61 | let expectation1 = expectation(description: "testRead") 62 | request.read(path: "https://foo.firebaseio.com/users/12345/double.json?access_token=accessToken", query: [:]) { result in 63 | switch result { 64 | case .failed: 65 | XCTFail() 66 | 67 | case .succeeded(let data): 68 | XCTAssertTrue(data is String) 69 | let number = Double(data as! String) 70 | XCTAssertNotNil(number) 71 | XCTAssertEqual(number, 101.12345) 72 | } 73 | expectation1.fulfill() 74 | } 75 | waitForExpectations(timeout: 1) 76 | } 77 | 78 | func testReadWithInvalidURL() { 79 | let request = Request.create() 80 | let expectation1 = expectation(description: "testRead") 81 | request.read(path: "", query: [:]) { result in 82 | switch result { 83 | case .succeeded: 84 | XCTFail() 85 | 86 | case .failed(let error): 87 | XCTAssertTrue(error is RequestError) 88 | let errorInfo = error as! RequestError 89 | XCTAssertTrue(errorInfo == RequestError.invalidURL) 90 | } 91 | expectation1.fulfill() 92 | } 93 | waitForExpectations(timeout: 1) 94 | } 95 | 96 | func testReadWithError() { 97 | let taskResult = URLSessionDataTaskMock.Result() 98 | let session = URLSessionMock() 99 | let operation = JSONRequestOperation.create() 100 | let response = RequestResponse() 101 | let request = Request(session: session, operation: operation, response: response) 102 | let expectation1 = expectation(description: "testRead") 103 | 104 | taskResult.error = URLSessionDataTaskMock.TaskMockError.mockError1 105 | session.expectedDataTaskResult = taskResult 106 | 107 | request.read(path: "https://foo.firebaseio.com/users/12345/double.json?access_token=accessToken", query: [:]) { result in 108 | switch result { 109 | case .succeeded: 110 | XCTFail() 111 | 112 | case .failed(let error): 113 | XCTAssertTrue(error is URLSessionDataTaskMock.TaskMockError) 114 | let errorInfo = error as! URLSessionDataTaskMock.TaskMockError 115 | XCTAssertTrue(errorInfo == URLSessionDataTaskMock.TaskMockError.mockError1) 116 | } 117 | expectation1.fulfill() 118 | } 119 | waitForExpectations(timeout: 1) 120 | } 121 | 122 | func testReadWithNoURLResponse() { 123 | let taskResult = URLSessionDataTaskMock.Result() 124 | let session = URLSessionMock() 125 | let operation = JSONRequestOperation.create() 126 | let response = RequestResponse() 127 | let request = Request(session: session, operation: operation, response: response) 128 | let expectation1 = expectation(description: "testRead") 129 | 130 | taskResult.response = nil 131 | session.expectedDataTaskResult = taskResult 132 | 133 | request.read(path: "https://foo.firebaseio.com/users/12345/double.json?access_token=accessToken", query: [:]) { result in 134 | switch result { 135 | case .succeeded: 136 | XCTFail() 137 | 138 | case .failed(let error): 139 | XCTAssertTrue(error is RequestError) 140 | let errorInfo = error as! RequestError 141 | XCTAssertTrue(errorInfo == RequestError.noURLResponse) 142 | } 143 | expectation1.fulfill() 144 | } 145 | waitForExpectations(timeout: 1) 146 | } 147 | 148 | func testReadWithNilData() { 149 | let taskResult = URLSessionDataTaskMock.Result() 150 | let session = URLSessionMock() 151 | let operation = JSONRequestOperation.create() 152 | let response = RequestResponse() 153 | let request = Request(session: session, operation: operation, response: response) 154 | let expectation1 = expectation(description: "testRead") 155 | 156 | taskResult.response = HTTPURLResponse() 157 | session.expectedDataTaskResult = taskResult 158 | 159 | request.read(path: "https://foo.firebaseio.com/users/12345/double.json?access_token=accessToken", query: [:]) { result in 160 | switch result { 161 | case .failed: 162 | XCTFail() 163 | 164 | case .succeeded(let info): 165 | XCTAssertTrue(info is [AnyHashable: Any]) 166 | let resultInfo = info as! [AnyHashable: Any] 167 | XCTAssertTrue(resultInfo.isEmpty) 168 | } 169 | expectation1.fulfill() 170 | } 171 | waitForExpectations(timeout: 1) 172 | } 173 | 174 | func testReadWithErrorParsingData() { 175 | let taskResult = URLSessionDataTaskMock.Result() 176 | let session = URLSessionMock() 177 | let operation = RequestOperationMock() 178 | let response = RequestResponse() 179 | let request = Request(session: session, operation: operation, response: response) 180 | let expectation1 = expectation(description: "testRead") 181 | 182 | taskResult.response = HTTPURLResponse() 183 | taskResult.data = Data(bytes: [1,2,3]) 184 | session.expectedDataTaskResult = taskResult 185 | 186 | request.read(path: "https://foo.firebaseio.com/users/12345/double.json?access_token=accessToken", query: [:]) { result in 187 | switch result { 188 | case .succeeded: 189 | XCTFail() 190 | 191 | case .failed(let info): 192 | XCTAssertTrue(info is RequestOperationMock.MockError) 193 | let errorInfo = info as! RequestOperationMock.MockError 194 | XCTAssertTrue(errorInfo == RequestOperationMock.MockError.failedToParse) 195 | } 196 | expectation1.fulfill() 197 | } 198 | waitForExpectations(timeout: 1) 199 | } 200 | 201 | func testReadWithJSONNull() { 202 | let taskResult = URLSessionDataTaskMock.Result() 203 | let session = URLSessionMock() 204 | let operation = JSONRequestOperation.create() 205 | let response = RequestResponse() 206 | let request = Request(session: session, operation: operation, response: response) 207 | let expectation1 = expectation(description: "testRead") 208 | 209 | taskResult.response = HTTPURLResponse() 210 | taskResult.data = "null".data(using: .utf8) 211 | session.expectedDataTaskResult = taskResult 212 | 213 | request.read(path: "https://foo.firebaseio.com/users/12345/double.json?access_token=accessToken", query: [:]) { result in 214 | switch result { 215 | case .succeeded: 216 | XCTFail() 217 | 218 | case .failed(let info): 219 | XCTAssertTrue(info is RequestError) 220 | let resultInfo = info as! RequestError 221 | XCTAssertTrue(resultInfo == RequestError.nullJSON) 222 | } 223 | expectation1.fulfill() 224 | } 225 | waitForExpectations(timeout: 1) 226 | } 227 | 228 | func testWriteWithInvalidURL() { 229 | let request = Request.create() 230 | let expectation1 = expectation(description: "testWrite") 231 | request.write(path: "", method: .post, data: [:]) { result in 232 | switch result { 233 | case .succeeded: 234 | XCTFail() 235 | 236 | case .failed(let error): 237 | XCTAssertTrue(error is RequestError) 238 | let errorInfo = error as! RequestError 239 | XCTAssertTrue(errorInfo == RequestError.invalidURL) 240 | } 241 | expectation1.fulfill() 242 | } 243 | waitForExpectations(timeout: 1) 244 | } 245 | 246 | func testWriteWithError() { 247 | let taskResult = URLSessionDataTaskMock.Result() 248 | let session = URLSessionMock() 249 | let operation = JSONRequestOperation.create() 250 | let response = RequestResponse() 251 | let request = Request(session: session, operation: operation, response: response) 252 | let expectation1 = expectation(description: "testWrite") 253 | 254 | taskResult.error = URLSessionDataTaskMock.TaskMockError.mockError1 255 | session.expectedDataTaskResult = taskResult 256 | 257 | request.write(path: "https://foo.firebaseio.com/users/12345/double.json?access_token=accessToken", method: .post, data: [:]) { result in 258 | switch result { 259 | case .succeeded: 260 | XCTFail() 261 | 262 | case .failed(let error): 263 | XCTAssertTrue(error is URLSessionDataTaskMock.TaskMockError) 264 | let errorInfo = error as! URLSessionDataTaskMock.TaskMockError 265 | XCTAssertTrue(errorInfo == URLSessionDataTaskMock.TaskMockError.mockError1) 266 | } 267 | expectation1.fulfill() 268 | } 269 | waitForExpectations(timeout: 1) 270 | } 271 | 272 | func testWriteWithNoURLResponse() { 273 | let taskResult = URLSessionDataTaskMock.Result() 274 | let session = URLSessionMock() 275 | let operation = JSONRequestOperation.create() 276 | let response = RequestResponse() 277 | let request = Request(session: session, operation: operation, response: response) 278 | let expectation1 = expectation(description: "testWrite") 279 | 280 | taskResult.response = nil 281 | session.expectedDataTaskResult = taskResult 282 | 283 | request.write(path: "https://foo.firebaseio.com/users/12345/double.json?access_token=accessToken", method: .post, data: [:]) { result in 284 | switch result { 285 | case .succeeded: 286 | XCTFail() 287 | 288 | case .failed(let error): 289 | XCTAssertTrue(error is RequestError) 290 | let errorInfo = error as! RequestError 291 | XCTAssertTrue(errorInfo == RequestError.noURLResponse) 292 | } 293 | expectation1.fulfill() 294 | } 295 | waitForExpectations(timeout: 1) 296 | } 297 | 298 | func testWriteWithNilData() { 299 | let taskResult = URLSessionDataTaskMock.Result() 300 | let session = URLSessionMock() 301 | let operation = JSONRequestOperation.create() 302 | let response = RequestResponse() 303 | let request = Request(session: session, operation: operation, response: response) 304 | let expectation1 = expectation(description: "testWrite") 305 | 306 | taskResult.response = HTTPURLResponse() 307 | session.expectedDataTaskResult = taskResult 308 | 309 | request.write(path: "https://foo.firebaseio.com/users/12345/double.json?access_token=accessToken", method: .post, data: [:]) { result in 310 | switch result { 311 | case .failed: 312 | XCTFail() 313 | 314 | case .succeeded(let info): 315 | XCTAssertTrue(info is [AnyHashable: Any]) 316 | let resultInfo = info as! [AnyHashable: Any] 317 | XCTAssertTrue(resultInfo.isEmpty) 318 | } 319 | expectation1.fulfill() 320 | } 321 | waitForExpectations(timeout: 1) 322 | } 323 | 324 | func testWriteWithErrorParsingData() { 325 | let taskResult = URLSessionDataTaskMock.Result() 326 | let session = URLSessionMock() 327 | let operation = RequestOperationMock() 328 | let response = RequestResponse() 329 | let request = Request(session: session, operation: operation, response: response) 330 | let expectation1 = expectation(description: "testWrite") 331 | 332 | taskResult.response = HTTPURLResponse() 333 | taskResult.data = Data(bytes: [1,2,3]) 334 | session.expectedDataTaskResult = taskResult 335 | 336 | request.write(path: "https://foo.firebaseio.com/users/12345/double.json?access_token=accessToken", method: .post, data: [:]) { result in 337 | switch result { 338 | case .succeeded: 339 | XCTFail() 340 | 341 | case .failed(let info): 342 | XCTAssertTrue(info is RequestOperationMock.MockError) 343 | let errorInfo = info as! RequestOperationMock.MockError 344 | XCTAssertTrue(errorInfo == RequestOperationMock.MockError.failedToParse) 345 | } 346 | expectation1.fulfill() 347 | } 348 | waitForExpectations(timeout: 1) 349 | } 350 | 351 | func testDeleteWithInvalidURL() { 352 | let request = Request.create() 353 | let expectation1 = expectation(description: "testDelete") 354 | request.delete(path: "") { result in 355 | switch result { 356 | case .succeeded: 357 | XCTFail() 358 | 359 | case .failed(let error): 360 | XCTAssertTrue(error is RequestError) 361 | let errorInfo = error as! RequestError 362 | XCTAssertTrue(errorInfo == RequestError.invalidURL) 363 | } 364 | expectation1.fulfill() 365 | } 366 | waitForExpectations(timeout: 1) 367 | } 368 | 369 | func testDeleteWithError() { 370 | let taskResult = URLSessionDataTaskMock.Result() 371 | let session = URLSessionMock() 372 | let operation = JSONRequestOperation.create() 373 | let response = RequestResponse() 374 | let request = Request(session: session, operation: operation, response: response) 375 | let expectation1 = expectation(description: "testDelete") 376 | 377 | taskResult.error = URLSessionDataTaskMock.TaskMockError.mockError1 378 | session.expectedDataTaskResult = taskResult 379 | 380 | request.delete(path: "https://foo.firebaseio.com/users/12345/double.json?access_token=accessToken") { result in 381 | switch result { 382 | case .succeeded: 383 | XCTFail() 384 | 385 | case .failed(let error): 386 | XCTAssertTrue(error is URLSessionDataTaskMock.TaskMockError) 387 | let errorInfo = error as! URLSessionDataTaskMock.TaskMockError 388 | XCTAssertTrue(errorInfo == URLSessionDataTaskMock.TaskMockError.mockError1) 389 | } 390 | expectation1.fulfill() 391 | } 392 | waitForExpectations(timeout: 1) 393 | } 394 | 395 | func testDeleteWithNoURLResponse() { 396 | let taskResult = URLSessionDataTaskMock.Result() 397 | let session = URLSessionMock() 398 | let operation = JSONRequestOperation.create() 399 | let response = RequestResponse() 400 | let request = Request(session: session, operation: operation, response: response) 401 | let expectation1 = expectation(description: "testDelete") 402 | 403 | taskResult.response = nil 404 | session.expectedDataTaskResult = taskResult 405 | 406 | request.delete(path: "https://foo.firebaseio.com/users/12345/double.json?access_token=accessToken") { result in 407 | switch result { 408 | case .succeeded: 409 | XCTFail() 410 | 411 | case .failed(let error): 412 | XCTAssertTrue(error is RequestError) 413 | let errorInfo = error as! RequestError 414 | XCTAssertTrue(errorInfo == RequestError.noURLResponse) 415 | } 416 | expectation1.fulfill() 417 | } 418 | waitForExpectations(timeout: 1) 419 | } 420 | 421 | func testDeleteNilData() { 422 | let taskResult = URLSessionDataTaskMock.Result() 423 | let session = URLSessionMock() 424 | let operation = JSONRequestOperation.create() 425 | let response = RequestResponse() 426 | let request = Request(session: session, operation: operation, response: response) 427 | let expectation1 = expectation(description: "testDelete") 428 | 429 | taskResult.response = HTTPURLResponse() 430 | session.expectedDataTaskResult = taskResult 431 | 432 | request.delete(path: "https://foo.firebaseio.com/users/12345/double.json?access_token=accessToken") { result in 433 | switch result { 434 | case .failed: 435 | XCTFail() 436 | 437 | case .succeeded(let info): 438 | XCTAssertTrue(info is [AnyHashable: Any]) 439 | let resultInfo = info as! [AnyHashable: Any] 440 | XCTAssertTrue(resultInfo.isEmpty) 441 | } 442 | expectation1.fulfill() 443 | } 444 | waitForExpectations(timeout: 1) 445 | } 446 | 447 | func testDeleteWithErrorParsingData() { 448 | let taskResult = URLSessionDataTaskMock.Result() 449 | let session = URLSessionMock() 450 | let operation = RequestOperationMock() 451 | let response = RequestResponse() 452 | let request = Request(session: session, operation: operation, response: response) 453 | let expectation1 = expectation(description: "testDelete") 454 | 455 | taskResult.response = HTTPURLResponse() 456 | taskResult.data = Data(bytes: [1,2,3]) 457 | session.expectedDataTaskResult = taskResult 458 | 459 | request.delete(path: "https://foo.firebaseio.com/users/12345/double.json?access_token=accessToken") { result in 460 | switch result { 461 | case .succeeded: 462 | XCTFail() 463 | 464 | case .failed(let info): 465 | XCTAssertTrue(info is RequestOperationMock.MockError) 466 | let errorInfo = info as! RequestOperationMock.MockError 467 | XCTAssertTrue(errorInfo == RequestOperationMock.MockError.failedToParse) 468 | } 469 | expectation1.fulfill() 470 | } 471 | waitForExpectations(timeout: 1) 472 | } 473 | 474 | func testDeleteWithJSONNull() { 475 | let taskResult = URLSessionDataTaskMock.Result() 476 | let session = URLSessionMock() 477 | let operation = JSONRequestOperation.create() 478 | let response = RequestResponse() 479 | let request = Request(session: session, operation: operation, response: response) 480 | let expectation1 = expectation(description: "testDelete") 481 | 482 | taskResult.response = HTTPURLResponse() 483 | taskResult.data = "null".data(using: .utf8) 484 | session.expectedDataTaskResult = taskResult 485 | 486 | request.delete(path: "https://foo.firebaseio.com/users/12345/double.json?access_token=accessToken") { result in 487 | switch result { 488 | case .failed: 489 | XCTFail() 490 | 491 | case .succeeded(let info): 492 | XCTAssertTrue(info is String) 493 | let resultInfo = info as! String 494 | XCTAssertEqual(resultInfo.lowercased(), "null") 495 | } 496 | expectation1.fulfill() 497 | } 498 | waitForExpectations(timeout: 1) 499 | } 500 | 501 | func testWriteHavingPOSTWithJSONNull() { 502 | let taskResult = URLSessionDataTaskMock.Result() 503 | let session = URLSessionMock() 504 | let operation = JSONRequestOperation.create() 505 | let response = RequestResponse() 506 | let request = Request(session: session, operation: operation, response: response) 507 | let expectation1 = expectation(description: "testWritePOST") 508 | 509 | taskResult.response = HTTPURLResponse() 510 | taskResult.data = "null".data(using: .utf8) 511 | session.expectedDataTaskResult = taskResult 512 | 513 | request.write(path: "https://foo.firebaseio.com/users/12345/double.json?access_token=accessToken", method: .post, data: [:]) { result in 514 | switch result { 515 | case .succeeded: 516 | XCTFail() 517 | 518 | case .failed(let info): 519 | XCTAssertTrue(info is RequestError) 520 | let resultInfo = info as! RequestError 521 | XCTAssertTrue(resultInfo == RequestError.nullJSON) 522 | } 523 | expectation1.fulfill() 524 | } 525 | waitForExpectations(timeout: 1) 526 | } 527 | 528 | func testWriteHavingPUTWithJSONNull() { 529 | let taskResult = URLSessionDataTaskMock.Result() 530 | let session = URLSessionMock() 531 | let operation = JSONRequestOperation.create() 532 | let response = RequestResponse() 533 | let request = Request(session: session, operation: operation, response: response) 534 | let expectation1 = expectation(description: "testWritePUT") 535 | 536 | taskResult.response = HTTPURLResponse() 537 | taskResult.data = "null".data(using: .utf8) 538 | session.expectedDataTaskResult = taskResult 539 | 540 | request.write(path: "https://foo.firebaseio.com/users/12345/double.json?access_token=accessToken", method: .put, data: [:]) { result in 541 | switch result { 542 | case .succeeded: 543 | XCTFail() 544 | 545 | case .failed(let info): 546 | XCTAssertTrue(info is RequestError) 547 | let resultInfo = info as! RequestError 548 | XCTAssertTrue(resultInfo == RequestError.nullJSON) 549 | } 550 | expectation1.fulfill() 551 | } 552 | waitForExpectations(timeout: 1) 553 | } 554 | 555 | func testWriteHavingPATCHWithJSONNull() { 556 | let taskResult = URLSessionDataTaskMock.Result() 557 | let session = URLSessionMock() 558 | let operation = JSONRequestOperation.create() 559 | let response = RequestResponse() 560 | let request = Request(session: session, operation: operation, response: response) 561 | let expectation1 = expectation(description: "testWritePATCH") 562 | 563 | taskResult.response = HTTPURLResponse() 564 | taskResult.data = "null".data(using: .utf8) 565 | session.expectedDataTaskResult = taskResult 566 | 567 | request.write(path: "https://foo.firebaseio.com/users/12345/double.json?access_token=accessToken", method: .patch, data: [:]) { result in 568 | switch result { 569 | case .succeeded: 570 | XCTFail() 571 | 572 | case .failed(let info): 573 | XCTAssertTrue(info is RequestError) 574 | let resultInfo = info as! RequestError 575 | XCTAssertTrue(resultInfo == RequestError.nullJSON) 576 | } 577 | expectation1.fulfill() 578 | } 579 | waitForExpectations(timeout: 1) 580 | } 581 | 582 | func testReadShouldReturnErroneous() { 583 | let taskResult = URLSessionDataTaskMock.Result() 584 | let session = URLSessionMock() 585 | let operation = JSONRequestOperation.create() 586 | let response = RequestResponse() 587 | let request = Request(session: session, operation: operation, response: response) 588 | let expectation1 = expectation(description: "testRead") 589 | 590 | taskResult.response = HTTPURLResponse(url: URL(string: "https://sampleio.com")!, statusCode: 400, httpVersion: nil, headerFields: nil) 591 | session.expectedDataTaskResult = taskResult 592 | 593 | request.read(path: "https://foo.firebaseio.com/users/12345/double.json?access_token=accessToken", query: [:]) { result in 594 | switch result { 595 | case .succeeded: 596 | XCTFail() 597 | 598 | case .failed(let info): 599 | XCTAssertTrue(info is RequestError) 600 | let resultInfo = info as! RequestError 601 | XCTAssertTrue(resultInfo == .badRequest("")) 602 | } 603 | expectation1.fulfill() 604 | } 605 | waitForExpectations(timeout: 1) 606 | } 607 | 608 | func testWriteShouldReturnErroneous() { 609 | let taskResult = URLSessionDataTaskMock.Result() 610 | let session = URLSessionMock() 611 | let operation = RequestOperationMock() 612 | let response = RequestResponse() 613 | let request = Request(session: session, operation: operation, response: response) 614 | let expectation1 = expectation(description: "testWrite") 615 | 616 | taskResult.response = HTTPURLResponse(url: URL(string: "https://sampleio.com")!, statusCode: 500, httpVersion: nil, headerFields: nil) 617 | session.expectedDataTaskResult = taskResult 618 | 619 | request.write(path: "https://foo.firebaseio.com/users/12345/double.json?access_token=accessToken", method: .post, data: [:]) { result in 620 | switch result { 621 | case .succeeded: 622 | XCTFail() 623 | 624 | case .failed(let info): 625 | XCTAssertTrue(info is RequestError) 626 | let errorInfo = info as! RequestError 627 | XCTAssertTrue(errorInfo == .internalServiceError("")) 628 | } 629 | expectation1.fulfill() 630 | } 631 | waitForExpectations(timeout: 1) 632 | } 633 | 634 | func testDeleteShouldReturnErroneous() { 635 | let taskResult = URLSessionDataTaskMock.Result() 636 | let session = URLSessionMock() 637 | let operation = RequestOperationMock() 638 | let response = RequestResponse() 639 | let request = Request(session: session, operation: operation, response: response) 640 | let expectation1 = expectation(description: "testDelete") 641 | 642 | taskResult.response = HTTPURLResponse(url: URL(string: "https://sampleio.com")!, statusCode: 404, httpVersion: nil, headerFields: nil) 643 | session.expectedDataTaskResult = taskResult 644 | 645 | request.delete(path: "https://foo.firebaseio.com/users/12345/double.json?access_token=accessToken") { result in 646 | switch result { 647 | case .succeeded: 648 | XCTFail() 649 | 650 | case .failed(let info): 651 | XCTAssertTrue(info is RequestError) 652 | let errorInfo = info as! RequestError 653 | XCTAssertTrue(errorInfo == .notFound("")) 654 | } 655 | expectation1.fulfill() 656 | } 657 | waitForExpectations(timeout: 1) 658 | } 659 | 660 | func testBuildURLforGETWithNonEmptyData() { 661 | let request = Request.create() 662 | var path = "https://foo.firebaseio.com/posts" 663 | var url = request.buildURL(path, .get, ["orderBy": "\"$key\"", "limitToFirst": 1]) 664 | XCTAssertNotNil(url) 665 | XCTAssertEqual(url!.absoluteString, "\(path)?orderBy=%22$key%22&limitToFirst=1") 666 | 667 | let accessToken = "accessToken" 668 | path = "https://foo.firebaseio.com/posts.json?auth=\(accessToken)" 669 | url = request.buildURL(path, .get, ["orderBy": "\"$key\"", "limitToFirst": 1]) 670 | XCTAssertNotNil(url) 671 | XCTAssertEqual(url!.absoluteString, "\(path)&orderBy=%22$key%22&limitToFirst=1") 672 | 673 | url = request.buildURL("", .get, ["orderBy": "\"$key\"", "limitToFirst": 1]) 674 | XCTAssertNil(url) 675 | } 676 | } 677 | -------------------------------------------------------------------------------- /PyrobaseTests/URLSessionDataDelegateMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLSessionDataDelegateMock.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 23/06/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class URLSessionDataDelegateMock: NSObject, URLSessionDataDelegate { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /PyrobaseTests/URLSessionMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLSessionMock.swift 3 | // Pyrobase 4 | // 5 | // Created by Mounir Ybanez on 02/05/2017. 6 | // Copyright © 2017 Ner. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class URLSessionMock: URLSession { 12 | 13 | var content = [ 14 | "https://foo.firebaseio.com/users/12345/name.json?access_token=accessToken": "Luche", 15 | "https://foo.firebaseio.com/users/12345/int.json?access_token=accessToken": "101", 16 | "https://foo.firebaseio.com/users/12345/double.json?access_token=accessToken": "101.12345" 17 | ] 18 | 19 | var expectedDataTaskResult: URLSessionDataTaskMock.Result? 20 | var shouldExecuteTaskResume: Bool = true 21 | 22 | override func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { 23 | let url = request.url! 24 | let task = URLSessionDataTaskMock(handler: completionHandler) 25 | task.handler = completionHandler 26 | 27 | if expectedDataTaskResult != nil { 28 | task.result = expectedDataTaskResult! 29 | 30 | } else { 31 | task.result.data = content[url.absoluteString]?.data(using: .utf8) 32 | task.result.response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil) 33 | task.result.error = nil 34 | } 35 | 36 | return task 37 | } 38 | 39 | override func dataTask(with url: URL) -> URLSessionDataTask { 40 | let task = URLSessionDataTaskMock(httpResponse: nil) 41 | task.shouldExecuteResume = shouldExecuteTaskResume 42 | return task 43 | } 44 | } 45 | 46 | class URLSessionDataTaskMock: URLSessionDataTask { 47 | 48 | enum TaskMockError: Error { 49 | 50 | case mockError1 51 | } 52 | 53 | class Result { 54 | 55 | var data: Data? 56 | var response: URLResponse? 57 | var error: Error? 58 | } 59 | 60 | var handler: (Data?, URLResponse?, Error?) -> Void 61 | var result: Result 62 | var httpResponse: HTTPURLResponse? 63 | var shouldExecuteResume: Bool = true 64 | 65 | override var response: URLResponse? { 66 | return httpResponse 67 | } 68 | 69 | init(handler: @escaping (Data?, URLResponse?, Error?) -> Void) { 70 | self.handler = handler 71 | self.result = Result() 72 | } 73 | 74 | convenience init(httpResponse: HTTPURLResponse?) { 75 | let handler: (Data?, URLResponse?, Error?) -> Void = { _, _, _ in } 76 | self.init(handler: handler) 77 | self.httpResponse = httpResponse 78 | } 79 | 80 | override func resume() { 81 | guard shouldExecuteResume else { 82 | return 83 | } 84 | 85 | handler(result.data, result.response, result.error) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pyrobase 2 | An iOS lightweight wrapper for Firebase REST API. For more Firebase's details, see it [here.](https://firebase.google.com/docs/reference/rest/database/) It is written in Swift and has 100% code coverage. 3 | 4 | ## Installation 5 | 6 | ### Cocoapods 7 | ```shell 8 | $ pod repo update 9 | $ pod init 10 | $ vim Podfile 11 | // Add `pod Pyrobase` in your podfile 12 | $ pod install 13 | ``` 14 | 15 | ### Carthage 16 | ```shell 17 | $ brew install carthage 18 | $ vim Cartfile 19 | // Add `github "mownier/Pyrobase" ~> 1.0` in your cartfile 20 | $ carthage update 21 | ``` 22 | 23 | ## Usage 24 | 25 | ### Authentication 26 | 27 | Make sure to copy `PyroAuthInfo.plist`. And keep in mind the bundle identifier where the said file is added. 28 | 29 | #### Initialization 30 | ```swift 31 | let apiKey = "yourFirebaseAPIKey" 32 | let bundleIdentifier = "com.my.app" 33 | let auth = PyroAuth.create(key: apiKey, bundleIdentifier: bundleIdentifier) 34 | // The variable 'auth' is nil if you provide an invalid bundle identifier. 35 | // Otherwise, you are good to go. 36 | // NOTE: If you build this project as framework, you can opt out 37 | // providing the bundle identifier. The default value 38 | // is the project's bundle identifier 39 | ``` 40 | 41 | #### Sign In 42 | ```swift 43 | auth.signIn(email: email, password: password) { result in 44 | switch result { 45 | case .failed(let error): 46 | print(error) 47 | // Do some stuff 48 | 49 | case .succeeded(let data): 50 | print(data) 51 | // Do some stuff 52 | // 'data' is PyroAuthContent 53 | } 54 | } 55 | ``` 56 | 57 | #### Register 58 | ```swift 59 | auth.register(email: email, password: password) { result in 60 | switch result { 61 | case .failed(let error): 62 | print(error) 63 | // Do some stuff 64 | 65 | case .succeeded(let data): 66 | print(data) 67 | // Do some stuff 68 | // 'data' is PyroAuthContent 69 | } 70 | } 71 | ``` 72 | 73 | #### Refresh Token 74 | ```swift 75 | auth.refresh(token: "refreshToken") { result in 76 | switch result { 77 | case .failed(let error): 78 | print(error) 79 | // Do some stuff 80 | 81 | case .succeeded(let data): 82 | print(data) 83 | // Do some stuff 84 | // 'data' is PyroAuthTokenContent 85 | } 86 | } 87 | ``` 88 | 89 | #### Send Password Reset 90 | ```swift 91 | auth.sendPasswordReset(email: "me@me.com") { result in 92 | switch result { 93 | case .failed(let error): 94 | print(error) 95 | // Do some stuff 96 | 97 | case .succeeded(let data): 98 | print(data) 99 | // Do some stuff 100 | // 'data' is Bool 101 | } 102 | } 103 | ``` 104 | 105 | ### REST 106 | 107 | #### Initialization 108 | ```swift 109 | let baseURL = "https://foo.firebaseio.com" 110 | let accessToken = "accessToken" 111 | let pyrobase = Pyrobase.create(baseURL: baseURL, accessToken: accessToken) 112 | ``` 113 | 114 | #### GET Request 115 | ```swift 116 | pyrobase.get(path: "users/abcde12345wert", query: [:]) { result in 117 | switch result { 118 | case .failed(let error): 119 | print(error) 120 | // Do some stuff 121 | 122 | case .succeeded(let data): 123 | print(data) 124 | // Do some stuff 125 | } 126 | } 127 | ``` 128 | 129 | #### POST Request 130 | ```swift 131 | pyrobase.post(path: "messages", value: ["message": "hello world", "user_id": "abcde12345qwert"]) { result in 132 | switch result { 133 | case .failed(let error): 134 | print(error) 135 | // Do some stuff 136 | 137 | case .succeeded(let data): 138 | print(data) 139 | // Do some stuff 140 | } 141 | } 142 | ``` 143 | 144 | #### PUT Request 145 | ```swift 146 | pyrobase.put(path: "users/abcde12345wert", value: ["first_name": "Juan", "last_name": "Dela Cruz"]) { result in 147 | switch result { 148 | case .failed(let error): 149 | print(error) 150 | // Do some stuff 151 | 152 | case .succeeded(let data): 153 | print(data) 154 | // Do some stuff 155 | } 156 | } 157 | ``` 158 | 159 | #### PATCH Request 160 | ```swift 161 | pyrobase.patch(path: "users/abcde12345wert", value: ["first_name": "Jose"]) { result in 162 | switch result { 163 | case .failed(let error): 164 | print(error) 165 | // Do some stuff 166 | 167 | case .succeeded(let data): 168 | print(data) 169 | // Do some stuff 170 | } 171 | } 172 | ``` 173 | 174 | ### Transaction 175 | 176 | #### Initialization 177 | ```swift 178 | let baseURL = "https://foo.firebaseio.com" 179 | let accessToken = "accessToken" 180 | let transaction = PyroTransaction.create(baseURL: baseURL, accessToken: accessToken) 181 | ``` 182 | #### Run 183 | ```swift 184 | transaction.run( 185 | parentPath: "posts/yuiop98765nbcwe", 186 | childKey: "likes_count", 187 | mutator: { data in 188 | let likesCount = data as! Int 189 | return likesCount + 1 190 | }) { result in 191 | switch result { 192 | case .failed(let error): 193 | print(error) 194 | // Do some stuff 195 | 196 | case .succeeded(let data): 197 | print(data) 198 | // Do some stuff 199 | } 200 | } 201 | ``` 202 | 203 | 204 | ### Event Source 205 | 206 | #### Callback 207 | ```swift 208 | class StreamCallback: PyroEventSourceCallback { 209 | 210 | func pyroEventSource(_ eventSource: PyroEventSource, didReceiveError error: Error) { 211 | // Do some stuff 212 | } 213 | 214 | func pyroEventSource(_ eventSource: PyroEventSource, didReceiveMessage message: PyroEventSourceMessage) { 215 | // Do some stuff 216 | } 217 | 218 | func pyroEventSourceOnOpen(_ eventSource: PyroEventSource) { 219 | // Do some stuff 220 | } 221 | 222 | func pyroEventSourceOnClosed(_ eventSource: PyroEventSource) { 223 | // Do some stuff 224 | } 225 | 226 | func pyroEventSourceOnConnecting(_ eventSource: PyroEventSource) { 227 | // Do some stuff 228 | } 229 | } 230 | ``` 231 | 232 | #### Initialization 233 | ```swift 234 | let callback = StreamCallback() 235 | let baseURL = "https://foo.firebaseio.com" 236 | let accessToken = "accessToken" 237 | let eventSource = PyroEventSource.create(baseURL: baseURL, accessToken: accessToken) 238 | eventSource.callback = callback 239 | ``` 240 | 241 | #### Stream 242 | ```swift 243 | eventSource.stream("chat/rooms/hdjye53910kwdop") 244 | ``` 245 | 246 | #### Close 247 | ```swift 248 | eventSource.close() 249 | ``` 250 | 251 | ### Query Parameters 252 | Always keep in mind of adding `.indexOn` in your rules for the path you want to query. You may receive a `badRequest` error if you don't set it. 253 | 254 | ```swift 255 | let query = ["orderBy": "\"$key\"", "limitToFirst": 1] 256 | pyrobase.get("posts", query: query) { result in 257 | switch result { 258 | case .failed(let error): 259 | print(error) 260 | // Do some stuff 261 | 262 | case .succeeded(let data): 263 | print(data) 264 | // Do some stuff 265 | } 266 | } 267 | ``` 268 | 269 | 270 | ### License 271 | 272 | MIT License --------------------------------------------------------------------------------