diff --git a/App/Assets.xcassets/AppIcon.appiconset/Contents.json b/App/Assets.xcassets/AppIcon.appiconset/Contents.json index 118c98f74..b8236c653 100644 --- a/App/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/App/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,5 +1,15 @@ { "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, { "idiom" : "iphone", "size" : "29x29", diff --git a/App/ViewController.swift b/App/ViewController.swift index c959469a5..9766845db 100644 --- a/App/ViewController.swift +++ b/App/ViewController.swift @@ -58,7 +58,6 @@ class ViewController: UIViewController { withImage: LazyImage(name: "ic_slack") ) } - .allowedConnections(["github", "instagram", "Username-Password-Authentication", "slack"]) }, actionButton(withTitle: "LOGIN WITH CUSTOM STYLE") { return Lock diff --git a/Lock.xcodeproj/project.pbxproj b/Lock.xcodeproj/project.pbxproj index a538d1711..6b091eaab 100644 --- a/Lock.xcodeproj/project.pbxproj +++ b/Lock.xcodeproj/project.pbxproj @@ -7,6 +7,13 @@ objects = { /* Begin PBXBuildFile section */ + 5B09717C1DC8F229003AA88F /* EnterpriseDomain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B09717B1DC8F229003AA88F /* EnterpriseDomain.swift */; }; + 5B09717E1DC8F292003AA88F /* EnterpriseDomainInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B09717D1DC8F292003AA88F /* EnterpriseDomainInteractor.swift */; }; + 5B0971801DC8F5C4003AA88F /* EnterpriseDomainPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B09717F1DC8F5C4003AA88F /* EnterpriseDomainPresenter.swift */; }; + 5B0971821DC8FAC5003AA88F /* EnterpriseDomainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B0971811DC8FAC5003AA88F /* EnterpriseDomainView.swift */; }; + 5BA563F11DD117550002D3AB /* EnterpriseDomainInteractorSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA563EF1DD1171F0002D3AB /* EnterpriseDomainInteractorSpec.swift */; }; + 5BC6BC0C1DCBDDC9002EA81C /* EnterpriseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC6BC0B1DCBDDC9002EA81C /* EnterpriseView.swift */; }; + 5BCED4C71DD1FEAA00E2CE8A /* EnterpriseDomainPresenterSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BCED4C51DD1FCF200E2CE8A /* EnterpriseDomainPresenterSpec.swift */; }; 5F14565A1D5130E80085DF9C /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F1456591D5130E80085DF9C /* Colors.swift */; }; 5F14565B1D5237180085DF9C /* LazyImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F92C68E1D50EAC200CCE6C0 /* LazyImage.swift */; }; 5F14565C1D5237210085DF9C /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F1456591D5130E80085DF9C /* Colors.swift */; }; @@ -191,6 +198,13 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 5B09717B1DC8F229003AA88F /* EnterpriseDomain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = EnterpriseDomain.swift; path = Lock/EnterpriseDomain.swift; sourceTree = SOURCE_ROOT; }; + 5B09717D1DC8F292003AA88F /* EnterpriseDomainInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = EnterpriseDomainInteractor.swift; path = Lock/EnterpriseDomainInteractor.swift; sourceTree = SOURCE_ROOT; }; + 5B09717F1DC8F5C4003AA88F /* EnterpriseDomainPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = EnterpriseDomainPresenter.swift; path = Lock/EnterpriseDomainPresenter.swift; sourceTree = SOURCE_ROOT; }; + 5B0971811DC8FAC5003AA88F /* EnterpriseDomainView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = EnterpriseDomainView.swift; path = Lock/EnterpriseDomainView.swift; sourceTree = SOURCE_ROOT; }; + 5BA563EF1DD1171F0002D3AB /* EnterpriseDomainInteractorSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnterpriseDomainInteractorSpec.swift; sourceTree = ""; }; + 5BC6BC0B1DCBDDC9002EA81C /* EnterpriseView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = EnterpriseView.swift; path = Lock/EnterpriseView.swift; sourceTree = SOURCE_ROOT; }; + 5BCED4C51DD1FCF200E2CE8A /* EnterpriseDomainPresenterSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnterpriseDomainPresenterSpec.swift; sourceTree = ""; }; 5F1456591D5130E80085DF9C /* Colors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Colors.swift; path = Lock/Colors.swift; sourceTree = SOURCE_ROOT; }; 5F14565D1D5237820085DF9C /* DatabaseView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DatabaseView.swift; path = Lock/DatabaseView.swift; sourceTree = SOURCE_ROOT; }; 5F1C498D1D8360AA005B74FC /* Style.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Style.swift; path = Lock/Style.swift; sourceTree = SOURCE_ROOT; }; @@ -364,9 +378,11 @@ 5FC4348E1D1E0A57005188BC /* DatabaseAuthenticable.swift */, 5F508FF61D1D868900EAA650 /* DatabaseInteractor.swift */, 5F73CDD91D30957900D8D8D1 /* DatabasePasswordInteractor.swift */, + 5B09717D1DC8F292003AA88F /* EnterpriseDomainInteractor.swift */, 5FBE5CBD1D3E5C7B0038536D /* MultifactorAuthenticatable.swift */, 5FBE5CBF1D3E5E0A0038536D /* MultifactorInteractor.swift */, 5F73CDD71D3093BF00D8D8D1 /* PasswordRecoverable.swift */, + 5B09717B1DC8F229003AA88F /* EnterpriseDomain.swift */, 5F57DFD11D4FE59800C54DA8 /* OAuth2Authenticatable.swift */, 5F57DFD31D4FE64700C54DA8 /* Auth0OAuth2Interactor.swift */, 5F2496B21D665A5600A1C6E2 /* DatabaseUserCreator.swift */, @@ -377,6 +393,7 @@ 5F508FF81D1DB01D00EAA650 /* Interactors */ = { isa = PBXGroup; children = ( + 5BA563EF1DD1171F0002D3AB /* EnterpriseDomainInteractorSpec.swift */, 5F508FF91D1DB1C200EAA650 /* DatabaseInteractorSpec.swift */, 5F73CDDB1D309BE900D8D8D1 /* DatabasePasswordInteractorSpec.swift */, 5FBE5CC11D3E5EF50038536D /* MultifactorInteractorSpec.swift */, @@ -402,6 +419,7 @@ children = ( 5F1C49921D8360DF005B74FC /* LoadingView.swift */, 5F73CDD31D3073BE00D8D8D1 /* DatabaseForgotPasswordView.swift */, + 5B0971811DC8FAC5003AA88F /* EnterpriseDomainView.swift */, 5F50900D1D1DF40400EAA650 /* DatabaseOnlyView.swift */, 5FBE5CB91D3E59B90038536D /* MultifactorCodeView.swift */, 5FC434851D1DF769005188BC /* View.swift */, @@ -423,6 +441,7 @@ isa = PBXGroup; children = ( 5F73CDDD1D30B16900D8D8D1 /* DatabaseForgotPasswordPresenterSpec.swift */, + 5BCED4C51DD1FCF200E2CE8A /* EnterpriseDomainPresenterSpec.swift */, 5F5F98D31D21E3890016FC22 /* DatabasePresenterSpec.swift */, 5FBE5CC91D3EA1380038536D /* MultifactorPresenterSpec.swift */, 5F57DFCD1D4FBE5A00C54DA8 /* AuthPresenterSpec.swift */, @@ -516,6 +535,7 @@ children = ( 5F1C498F1D8360BF005B74FC /* ConnectionLoadingPresenter.swift */, 5F73CDD51D30790500D8D8D1 /* DatabaseForgotPasswordPresenter.swift */, + 5B09717F1DC8F5C4003AA88F /* EnterpriseDomainPresenter.swift */, 5FC434891D1DF82A005188BC /* DatabasePresenter.swift */, 5F73CDCF1D30250900D8D8D1 /* MessagePresenter.swift */, 5FBE5CC51D3E7F9D0038536D /* MultifactorPresenter.swift */, @@ -613,6 +633,7 @@ 5F99AA931D1BABFC00D27842 /* SecondaryButton.swift */, 5F51EE671D1C88FC0024BCD6 /* SignUpView.swift */, 5F51EE691D1CBC830024BCD6 /* SingleInputView.swift */, + 5BC6BC0B1DCBDDC9002EA81C /* EnterpriseView.swift */, 5FD6772B1D4C303C004B87C4 /* AuthButton.swift */, ); path = Components; @@ -881,6 +902,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 5B09717E1DC8F292003AA88F /* EnterpriseDomainInteractor.swift in Sources */, 5F5F98DA1D22EF490016FC22 /* Router.swift in Sources */, 5F14565A1D5130E80085DF9C /* Colors.swift in Sources */, 5F57DFC91D4F87CE00C54DA8 /* AuthCollectionView.swift in Sources */, @@ -910,8 +932,10 @@ 5F2496B61D665AA800A1C6E2 /* InputValidationError.swift in Sources */, 5FFC54FE1D37E3F700579581 /* Routes.swift in Sources */, 5F1C49931D8360DF005B74FC /* LoadingView.swift in Sources */, + 5B0971801DC8F5C4003AA88F /* EnterpriseDomainPresenter.swift in Sources */, 5FC434861D1DF769005188BC /* View.swift in Sources */, 5FDB41CE1D2C79FD00166B67 /* Operations.swift in Sources */, + 5B09717C1DC8F229003AA88F /* EnterpriseDomain.swift in Sources */, 5F51EE681D1C88FC0024BCD6 /* SignUpView.swift in Sources */, 5F57DFD41D4FE64700C54DA8 /* Auth0OAuth2Interactor.swift in Sources */, 5F57DFD21D4FE59800C54DA8 /* OAuth2Authenticatable.swift in Sources */, @@ -922,8 +946,10 @@ 5F92C68F1D50EAC200CCE6C0 /* LazyImage.swift in Sources */, 5F2496BA1D665AE900A1C6E2 /* DatabaseAuthenticatableError.swift in Sources */, 5F57DFC61D4F79DD00C54DA8 /* AuthStyle.swift in Sources */, + 5B0971821DC8FAC5003AA88F /* EnterpriseDomainView.swift in Sources */, 5FBE5CC61D3E7F9D0038536D /* MultifactorPresenter.swift in Sources */, 5FEAE2101D1A5691005C0028 /* HeaderView.swift in Sources */, + 5BC6BC0C1DCBDDC9002EA81C /* EnterpriseView.swift in Sources */, 5F99AA8C1D1B3F1300D27842 /* InputField.swift in Sources */, 5FBE5CBA1D3E59B90038536D /* MultifactorCodeView.swift in Sources */, 5F1C49901D8360BF005B74FC /* ConnectionLoadingPresenter.swift in Sources */, @@ -956,6 +982,7 @@ 5F6C01551D91656100198ACD /* UsernameValidatorSpec.swift in Sources */, 5F2496BE1D67ADB300A1C6E2 /* EmailValidatorSpec.swift in Sources */, 5FBE5CCA1D3EA1380038536D /* MultifactorPresenterSpec.swift in Sources */, + 5BA563F11DD117550002D3AB /* EnterpriseDomainInteractorSpec.swift in Sources */, 5F92C68D1D50E47100CCE6C0 /* AuthStyleSpec.swift in Sources */, 5F92C68B1D4FE90F00CCE6C0 /* Auth0OAuth2InteractorSpec.swift in Sources */, 5F5090081D1DE7BA00EAA650 /* NetworkStub.swift in Sources */, @@ -973,6 +1000,7 @@ 5FBE5CC81D3EA0EA0038536D /* Mocks.swift in Sources */, 5F57DFCE1D4FBE5A00C54DA8 /* AuthPresenterSpec.swift in Sources */, 5F92C6911D510AFE00CCE6C0 /* LazyImageSpec.swift in Sources */, + 5BCED4C71DD1FEAA00E2CE8A /* EnterpriseDomainPresenterSpec.swift in Sources */, 5F50900A1D1DEE9A00EAA650 /* Constants.swift in Sources */, 5FE50DBD1D79B8AD00D82290 /* CDNLoaderInteractorSpec.swift in Sources */, 5F390E8D1D63B99300FC549C /* LoggerSpec.swift in Sources */, diff --git a/Lock/CDNLoaderInteractor.swift b/Lock/CDNLoaderInteractor.swift index 61074c3f3..63692bf37 100644 --- a/Lock/CDNLoaderInteractor.swift +++ b/Lock/CDNLoaderInteractor.swift @@ -82,6 +82,12 @@ struct CDNLoaderInteractor: RemoteConnectionLoader, Loggable { connections.database(name: connection.name, requiresUsername: requiresUsername, usernameValidator: connection.usernameValidation) } } + info.enterprise.forEach { strategy in + strategy.connections.forEach { connection in + let domain = connection.json["domain_aliases"] as! [String] + connections.enterprise(name: connection.name, domains: domain) + } + } info.oauth2.forEach { strategy in strategy.connections.forEach { connections.social(name: $0.name, style: AuthStyle.style(forStrategy: strategy.name, connectionName: $0.name)) } } @@ -108,6 +114,8 @@ private struct ClientInfo { var auth0: StrategyInfo? { return strategies.filter({ $0.name == "auth0" }).first } var oauth2: [StrategyInfo] { return strategies.filter { $0.name != "auth0" && !passwordlessStrategyNames.contains($0.name) && !enterpriseStrategyNames.contains($0.name) } } + + var enterprise: [StrategyInfo] { return strategies.filter { $0.name != "auth0" && !passwordlessStrategyNames.contains($0.name) && enterpriseStrategyNames.contains($0.name) } } let passwordlessStrategyNames = [ "email", diff --git a/Lock/ConnectionBuildable.swift b/Lock/ConnectionBuildable.swift index f5cdd7a86..0997d0453 100644 --- a/Lock/ConnectionBuildable.swift +++ b/Lock/ConnectionBuildable.swift @@ -55,6 +55,14 @@ public protocol ConnectionBuildable: Connections { - seeAlso: AuthStyle */ mutating func oauth2(name name: String, style: AuthStyle) + + /** + Adds a new enterprise connection + + - parameter name: name of the connection + - parameter domain: array of enterprise domains + */ + mutating func enterprise(name name: String, domains: [String]) } public extension ConnectionBuildable { diff --git a/Lock/ConnectionLoadingPresenter.swift b/Lock/ConnectionLoadingPresenter.swift index 231c16cbf..59df78f83 100644 --- a/Lock/ConnectionLoadingPresenter.swift +++ b/Lock/ConnectionLoadingPresenter.swift @@ -42,4 +42,4 @@ class ConnectionLoadingPresenter: Presentable, Loggable { } return LoadingView() } -} \ No newline at end of file +} diff --git a/Lock/Connections.swift b/Lock/Connections.swift index 5cc9ed50a..ce5a4ab14 100644 --- a/Lock/Connections.swift +++ b/Lock/Connections.swift @@ -25,6 +25,7 @@ import Foundation public protocol Connections { var database: DatabaseConnection? { get } var oauth2: [OAuth2Connection] { get } + var enterprise: [EnterpriseConnection] {get} var isEmpty: Bool { get } @@ -59,3 +60,9 @@ public struct SocialConnection: OAuth2Connection { public let name: String public let style: AuthStyle } + +public struct EnterpriseConnection : OAuth2Connection { + public let name: String + public var style: AuthStyle { return AuthStyle(name: self.name) } + public let domains: [String] +} diff --git a/Lock/DatabasePasswordInteractor.swift b/Lock/DatabasePasswordInteractor.swift index 3d8ed4e2b..06c957b92 100644 --- a/Lock/DatabasePasswordInteractor.swift +++ b/Lock/DatabasePasswordInteractor.swift @@ -50,12 +50,12 @@ struct DatabasePasswordInteractor: PasswordRecoverable { func requestEmail(callback: (PasswordRecoverableError?) -> ()) { guard let email = self.email else { return callback(.NonValidInput) } guard let connection = self.connections.database?.name else { return callback(.NoDatabaseConnection) } - + self.authentication .resetPassword(email: email, connection: connection) .start { guard case .Success = $0 else { return callback(.EmailNotSent) } callback(nil) - } + } } -} \ No newline at end of file +} diff --git a/Lock/EnterpriseDomain.swift b/Lock/EnterpriseDomain.swift new file mode 100644 index 000000000..e5b7235c8 --- /dev/null +++ b/Lock/EnterpriseDomain.swift @@ -0,0 +1,32 @@ +// EnterpriseDomain.swift +// +// Copyright (c) 2016 Auth0 (http://auth0.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +protocol HRDAuthenticatable { + var email: String? { get } + var validEmail: Bool { get } + + mutating func updateEmail(value: String?) throws + + func login(callback: (OAuth2AuthenticatableError?) -> ()) +} diff --git a/Lock/EnterpriseDomainInteractor.swift b/Lock/EnterpriseDomainInteractor.swift new file mode 100644 index 000000000..10223eada --- /dev/null +++ b/Lock/EnterpriseDomainInteractor.swift @@ -0,0 +1,67 @@ +// EnterpriseDomainInteractor.swift +// +// Copyright (c) 2016 Auth0 (http://auth0.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import Auth0 + +struct EnterpriseDomainInteractor: HRDAuthenticatable { + + var email: String? = nil + var validEmail: Bool = false + var connection: EnterpriseConnection? = nil + + let connections: [EnterpriseConnection] + let emailValidator: InputValidator = EmailValidator() + let authenticator: OAuth2Authenticatable + + init(connections: [EnterpriseConnection], authentication: OAuth2Authenticatable) { + self.connections = connections + self.authenticator = authentication + } + + private func validateDomain(connections: [EnterpriseConnection], value: String?) -> EnterpriseConnection? { + + guard let domain = value?.componentsSeparatedByString("@").last else { return nil } + return connections.filter { $0.domains.contains(domain) }.first + } + + mutating func updateEmail(value: String?) throws { + + validEmail = false + connection = nil + + email = value?.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet()) + if let error = emailValidator.validate(value) { + throw error + } + validEmail = true + + self.connection = validateDomain(connections, value: email) + } + + func login(callback: (OAuth2AuthenticatableError?) -> ()) { + guard let connection = self.connection else { return callback(.NoConnectionAvailable) } + + authenticator.login(connection.name, callback: callback) + + } +} diff --git a/Lock/EnterpriseDomainPresenter.swift b/Lock/EnterpriseDomainPresenter.swift new file mode 100644 index 000000000..ca819dcbf --- /dev/null +++ b/Lock/EnterpriseDomainPresenter.swift @@ -0,0 +1,81 @@ +// EnterpriseDomainPresenter.swift +// +// Copyright (c) 2016 Auth0 (http://auth0.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +class EnterpriseDomainPresenter: Presentable, Loggable { + + var interactor: EnterpriseDomainInteractor + var customLogger: Logger? + + init(interactor: EnterpriseDomainInteractor) { + self.interactor = interactor + } + + var messagePresenter: MessagePresenter? + + var view: View { + let email = self.interactor.validEmail ? self.interactor.email : nil + let view = EnterpriseDomainView(email: email) + let form = view.form + view.form?.onValueChange = { input in + self.messagePresenter?.hideCurrent() + + guard case .Email = input.type else { return } + do { + try self.interactor.updateEmail(input.text) + input.showValid() + if let connection = self.interactor.connection { + self.logger.debug("Enterprise connection match: \(connection)") + } + } catch { + input.showError() + } + } + + let action = { (button: PrimaryButton) in + self.messagePresenter?.hideCurrent() + self.logger.info("Enterprise connection started: \(self.interactor.email), \(self.interactor.connection)") + let interactor = self.interactor + button.inProgress = true + interactor.login { error in + Queue.main.async { + button.inProgress = false + form?.needsToUpdateState() + if let error = error { + self.messagePresenter?.showError(error) + self.logger.error("Enterprise connection failed: \(error)") + } else { + self.logger.debug("Enterprise authenticator launched") + } + } + + } + } + view.primaryButton?.onPress = action + view.form?.onReturn = {_ in + guard let button = view.primaryButton else { return } + action(button) + } + return view + } +} diff --git a/Lock/EnterpriseDomainView.swift b/Lock/EnterpriseDomainView.swift new file mode 100644 index 000000000..e9b7d3c5b --- /dev/null +++ b/Lock/EnterpriseDomainView.swift @@ -0,0 +1,73 @@ +// EnterpriseDomainView.swift +// +// Copyright (c) 2016 Auth0 (http://auth0.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import UIKit + +class EnterpriseDomainView: UIView, View { + + weak var form: Form? + weak var primaryButton: PrimaryButton? + + init(email: String?) { + let primaryButton = PrimaryButton() + let domainView = EnterpriseView() + let center = UILayoutGuide() + + self.primaryButton = primaryButton + self.form = domainView + + super.init(frame: CGRectZero) + + self.addSubview(domainView) + self.addSubview(primaryButton) + self.addLayoutGuide(center) + + constraintEqual(anchor: center.leftAnchor, toAnchor: self.leftAnchor, constant: 20) + constraintEqual(anchor: center.topAnchor, toAnchor: self.topAnchor) + constraintEqual(anchor: center.rightAnchor, toAnchor: self.rightAnchor, constant: -20) + constraintEqual(anchor: center.bottomAnchor, toAnchor: primaryButton.topAnchor) + + constraintEqual(anchor: domainView.leftAnchor, toAnchor: center.leftAnchor) + constraintEqual(anchor: domainView.rightAnchor, toAnchor: center.rightAnchor) + constraintEqual(anchor: domainView.centerYAnchor, toAnchor: center.centerYAnchor) + constraintGreaterOrEqual(anchor: domainView.topAnchor, toAnchor: center.topAnchor, constant: 10, priority: UILayoutPriorityDefaultLow - 1) + constraintGreaterOrEqual(anchor: domainView.bottomAnchor, toAnchor: center.bottomAnchor, constant: -10, priority: UILayoutPriorityDefaultLow - 1) + domainView.translatesAutoresizingMaskIntoConstraints = false + + constraintEqual(anchor: primaryButton.leftAnchor, toAnchor: self.leftAnchor) + constraintEqual(anchor: primaryButton.rightAnchor, toAnchor: self.rightAnchor) + constraintEqual(anchor: primaryButton.bottomAnchor, toAnchor: self.bottomAnchor) + primaryButton.translatesAutoresizingMaskIntoConstraints = false + + domainView.type = .Email + domainView.returnKey = .Done + domainView.value = email + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func apply(style style: Style) { + self.primaryButton?.apply(style: style) + } +} diff --git a/Lock/EnterpriseView.swift b/Lock/EnterpriseView.swift new file mode 100644 index 000000000..7cfc59bf4 --- /dev/null +++ b/Lock/EnterpriseView.swift @@ -0,0 +1,108 @@ +// EnterpriseView.swift +// +// Copyright (c) 2016 Auth0 (http://auth0.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import UIKit + +public class EnterpriseView: UIView, Form { + private var inputField: InputField + private var stackView: UIStackView + + var value: String? { + get { + return self.inputField.text + } + set { + self.inputField.text = newValue + } + } + + var type: InputField.InputType = .Email { + didSet { + self.inputField.type = self.type + } + } + + var returnKey: UIReturnKeyType = .Done { + didSet { + self.inputField.returnKey = self.returnKey + } + } + + var onValueChange: (InputField) -> () = { _ in } { + didSet { + self.inputField.onTextChange = self.onValueChange + } + } + + var onReturn: (InputField) -> () { + get { + return self.inputField.onReturn + } + set { + self.inputField.onReturn = newValue + } + } + + func needsToUpdateState() { + self.inputField.needsToUpdateState() + } + + // MARK:- Initialisers + + required override public init(frame: CGRect) { + self.inputField = InputField() + self.stackView = UIStackView(arrangedSubviews: [inputField]) + super.init(frame: frame) + self.layoutForm() + } + + public convenience init() { + self.init(frame: CGRectZero) + } + + public required convenience init?(coder aDecoder: NSCoder) { + self.init(frame: CGRectZero) + } + + // MARK:- Layout + + private func layoutForm() { + self.addSubview(self.stackView) + + constraintEqual(anchor: self.stackView.leftAnchor, toAnchor: self.leftAnchor) + constraintEqual(anchor: self.stackView.topAnchor, toAnchor: self.topAnchor) + constraintEqual(anchor: self.stackView.rightAnchor, toAnchor: self.rightAnchor) + constraintEqual(anchor: self.stackView.bottomAnchor, toAnchor: self.bottomAnchor) + self.stackView.translatesAutoresizingMaskIntoConstraints = false + + self.stackView.alignment = .Fill + self.stackView.axis = .Vertical + self.stackView.distribution = .EqualCentering + + inputField.type = self.type + inputField.returnKey = self.returnKey + } + + public override func intrinsicContentSize() -> CGSize { + return CGSize(width: UIViewNoIntrinsicMetric, height: 244) + } +} diff --git a/Lock/OAuth2Authenticatable.swift b/Lock/OAuth2Authenticatable.swift index e1653d3e8..fcae567ef 100644 --- a/Lock/OAuth2Authenticatable.swift +++ b/Lock/OAuth2Authenticatable.swift @@ -27,11 +27,14 @@ protocol OAuth2Authenticatable { } enum OAuth2AuthenticatableError: ErrorType, LocalizableError { + case NoConnectionAvailable case CouldNotAuthenticate case Cancelled var localizableMessage: String { switch self { + case .NoConnectionAvailable: + return "We're sorry, we could not find a valid connection for this user.".i18n(key: "com.auth0.lock.error.authentication.noconnection", comment: "No valid connection") case .CouldNotAuthenticate: return "We're sorry, something went wrong when attempting to log in.".i18n(key: "com.auth0.lock.error.authentication.fallback", comment: "Generic login error") default: @@ -47,4 +50,4 @@ enum OAuth2AuthenticatableError: ErrorType, LocalizableError { return false } } -} \ No newline at end of file +} diff --git a/Lock/OfflineConnections.swift b/Lock/OfflineConnections.swift index 2cff86d9a..db25bdb6c 100644 --- a/Lock/OfflineConnections.swift +++ b/Lock/OfflineConnections.swift @@ -27,6 +27,7 @@ struct OfflineConnections: ConnectionBuildable { private (set) var databases: [DatabaseConnection] = [] var database: DatabaseConnection? { return self.databases.first } private (set) var oauth2: [OAuth2Connection] = [] + private (set) var enterprise: [EnterpriseConnection] = [] mutating func database(name name: String, requiresUsername: Bool, usernameValidator: UsernameValidator = UsernameValidator()) { self.databases.append(DatabaseConnection(name: name, requiresUsername: requiresUsername, usernameValidator: usernameValidator)) @@ -40,15 +41,21 @@ struct OfflineConnections: ConnectionBuildable { let social = SocialConnection(name: name, style: style) self.oauth2.append(social) } + + mutating func enterprise(name name: String, domains: [String]) { + let enterprise = EnterpriseConnection(name: name, domains: domains) + self.enterprise.append(enterprise) + } var isEmpty: Bool { - return self.database == nil && self.oauth2.isEmpty + return self.database == nil && self.oauth2.isEmpty && self.enterprise.isEmpty } func select(byNames names: [String]) -> OfflineConnections { var connections = OfflineConnections() connections.databases = self.databases.filter { isWhitelisted(connectionName: $0.name, inList: names) } connections.oauth2 = self.oauth2.filter { isWhitelisted(connectionName: $0.name, inList: names) } + connections.enterprise = self.enterprise.filter { isWhitelisted(connectionName: $0.name, inList: names) } return connections } } diff --git a/Lock/Router.swift b/Lock/Router.swift index c0b4a9312..ceb765f9a 100644 --- a/Lock/Router.swift +++ b/Lock/Router.swift @@ -81,7 +81,6 @@ struct Router: Navigable { let interactor = CDNLoaderInteractor(baseURL: self.lock.authentication.url, clientId: self.lock.authentication.clientId) return ConnectionLoadingPresenter(loader: interactor, navigator: self) } - if let database = connections.database { guard self.lock.options.allow != [.ResetPassword] && self.lock.options.initialScreen != .ResetPassword else { return forgotPassword } let authentication = self.lock.authentication @@ -93,7 +92,12 @@ struct Router: Navigable { } return presenter } - + if !connections.enterprise.isEmpty { + let authInteractor = Auth0OAuth2Interactor(webAuth: self.lock.webAuth, onCredentials: self.onAuthentication, options: self.lock.options) + let interactor = EnterpriseDomainInteractor(connections: connections.enterprise, authentication: authInteractor) + let presenter = EnterpriseDomainPresenter(interactor: interactor) + return presenter + } if !connections.oauth2.isEmpty { let interactor = Auth0OAuth2Interactor(webAuth: self.lock.webAuth, onCredentials: self.onAuthentication, options: self.lock.options) let presenter = AuthPresenter(connections: connections, interactor: interactor, customStyle: self.lock.style.oauth2) diff --git a/LockTests/Interactors/CDNLoaderInteractorSpec.swift b/LockTests/Interactors/CDNLoaderInteractorSpec.swift index 010779bc1..c76edb20e 100644 --- a/LockTests/Interactors/CDNLoaderInteractorSpec.swift +++ b/LockTests/Interactors/CDNLoaderInteractorSpec.swift @@ -27,106 +27,109 @@ import OHHTTPStubs @testable import Lock class CDNLoaderInteractorSpec: QuickSpec { - + override func spec() { - + afterEach { Auth0Stubs.cleanAll() Auth0Stubs.failUnknown() } - + describe("init") { - + it("should build url from non-auth0 domain") { let loader = CDNLoaderInteractor(baseURL: NSURL(string: "https://somewhere.far.beyond")!, clientId: clientId) expect(loader.url.absoluteString) == "https://somewhere.far.beyond/client/\(clientId).js" } - + it("should build url from auth0 domain") { let loader = CDNLoaderInteractor(baseURL: NSURL(string: "https://samples.auth0.com")!, clientId: clientId) expect(loader.url.absoluteString) == "https://cdn.auth0.com/client/\(clientId).js" } - + it("should build url from auth0 domain for eu region") { let loader = CDNLoaderInteractor(baseURL: NSURL(string: "https://samples.eu.auth0.com")!, clientId: clientId) expect(loader.url.absoluteString) == "https://cdn.eu.auth0.com/client/\(clientId).js" } - + it("should build url from auth0 domain for au region") { let loader = CDNLoaderInteractor(baseURL: NSURL(string: "https://samples.au.auth0.com")!, clientId: clientId) expect(loader.url.absoluteString) == "https://cdn.au.auth0.com/client/\(clientId).js" } - + } - + describe("load") { - + var loader: CDNLoaderInteractor! var connections: Connections? var callback: (Connections? -> ())! - + beforeEach { loader = CDNLoaderInteractor(baseURL: NSURL(string: "https://overmind.auth0.com")!, clientId: clientId) callback = { connections = $0 } connections = nil } - + context("failure") { - + beforeEach { connections = OfflineConnections() } - + it("should fail") { loader.load(callback) expect(connections).toEventually(beNil()) } - + it("should fail for status code not in range 200...299") { stub(isCDN(forClientId: clientId)) { _ in OHHTTPStubsResponse(data: NSData(), statusCode: 400, headers: [:]) } loader.load(callback) expect(connections).toEventually(beNil()) } - + it("should fail when there is no body") { stub(isCDN(forClientId: clientId)) { _ in OHHTTPStubsResponse(data: NSData(), statusCode: 200, headers: [:]) } loader.load(callback) expect(connections).toEventually(beNil()) } - + it("should fail for invalid json") { stub(isCDN(forClientId: clientId)) { _ in OHHTTPStubsResponse(data: "not a json object".dataUsingEncoding(NSUTF8StringEncoding)!, statusCode: 200, headers: [:]) } loader.load(callback) expect(connections).toEventually(beNil()) } } - + let databaseConnection = "DB Connection" - + it("should load empty strategies") { stub(isCDN(forClientId: clientId)) { _ in Auth0Stubs.strategiesFromCDN([]) } loader.load(callback) expect(connections).toEventuallyNot(beNil()) expect(connections?.database).toEventually(beNil()) expect(connections?.oauth2).toEventually(beEmpty()) + expect(connections?.enterprise).toEventually(beEmpty()) } - + it("should not load strategies without name") { stub(isCDN(forClientId: clientId)) { _ in Auth0Stubs.strategiesFromCDN([[:]]) } loader.load(callback) expect(connections).toEventuallyNot(beNil()) expect(connections?.database).toEventually(beNil()) expect(connections?.oauth2).toEventually(beEmpty()) + expect(connections?.enterprise).toEventually(beEmpty()) } - + it("should not load connection without name") { stub(isCDN(forClientId: clientId)) { _ in Auth0Stubs.strategiesFromCDN([mockStrategy("auth0", connections: [[:]])]) } loader.load(callback) expect(connections).toEventuallyNot(beNil()) expect(connections?.database).toEventually(beNil()) expect(connections?.oauth2).toEventually(beEmpty()) + expect(connections?.enterprise).toEventually(beEmpty()) } - + it("should load single database connection") { stub(isCDN(forClientId: clientId)) { _ in return Auth0Stubs.strategiesFromCDN([mockStrategy("auth0", connections: [mockDatabaseConnection(databaseConnection)])]) } loader.load(callback) @@ -134,7 +137,9 @@ class CDNLoaderInteractorSpec: QuickSpec { expect(connections?.database?.name).toEventually(equal(databaseConnection)) expect(connections?.database?.requiresUsername).toEventually(beFalsy()) } - + + // MARK: Database + it("should load single database connection with custom username validation") { stub(isCDN(forClientId: clientId)) { _ in return Auth0Stubs.strategiesFromCDN([mockStrategy("auth0", connections: [mockDatabaseConnection(databaseConnection, validation: ["username": ["min": 10, "max": 200]])])]) } loader.load(callback) @@ -145,7 +150,7 @@ class CDNLoaderInteractorSpec: QuickSpec { expect(validator?.range.startIndex) == 10 expect(validator?.range.endIndex) == 201 } - + it("should load single database connection with custom username validation with strings") { stub(isCDN(forClientId: clientId)) { _ in return Auth0Stubs.strategiesFromCDN([mockStrategy("auth0", connections: [mockDatabaseConnection(databaseConnection, validation: ["username": ["min": "9", "max": "100"]])])]) } loader.load(callback) @@ -156,14 +161,14 @@ class CDNLoaderInteractorSpec: QuickSpec { expect(validator?.range.startIndex) == 9 expect(validator?.range.endIndex) == 101 } - + it("should load multiple database connections but pick the first") { stub(isCDN(forClientId: clientId)) { _ in return Auth0Stubs.strategiesFromCDN([mockStrategy("auth0", connections: [mockDatabaseConnection(databaseConnection), mockDatabaseConnection("another one")])]) } loader.load(callback) expect(connections?.database).toEventuallyNot(beNil()) expect(connections?.database?.name).toEventually(equal(databaseConnection)) } - + it("should load single database connection with requires_username") { stub(isCDN(forClientId: clientId)) { _ in return Auth0Stubs.strategiesFromCDN([mockStrategy("auth0", connections: [mockDatabaseConnection(databaseConnection, requiresUsername: true)])]) } loader.load(callback) @@ -171,7 +176,9 @@ class CDNLoaderInteractorSpec: QuickSpec { expect(connections?.database?.name).toEventually(equal(databaseConnection)) expect(connections?.database?.requiresUsername).toEventually(beTruthy()) } - + + // MARK: OAuth2 + it("should load oauth2 connections") { stub(isCDN(forClientId: clientId)) { _ in return Auth0Stubs.strategiesFromCDN([mockStrategy("oauth2", connections: [mockOAuth2("steam")])]) } loader.load(callback) @@ -181,7 +188,7 @@ class CDNLoaderInteractorSpec: QuickSpec { expect(oauth2?.style.name) == "steam" expect(oauth2?.style.normalColor) == .a0_orange } - + it("should load first class social connections") { stub(isCDN(forClientId: clientId)) { _ in return Auth0Stubs.strategiesFromCDN([mockStrategy("github", connections: [mockOAuth2("random")])]) } loader.load(callback) @@ -190,7 +197,7 @@ class CDNLoaderInteractorSpec: QuickSpec { expect(oauth2?.name) == "random" expect(oauth2?.style) == .Github } - + it("should load multiple oauth2 connections") { stub(isCDN(forClientId: clientId)) { _ in return Auth0Stubs.strategiesFromCDN([mockStrategy("facebook", connections: [mockOAuth2("facebook1"), mockOAuth2("facebook2")])]) } loader.load(callback) @@ -199,23 +206,99 @@ class CDNLoaderInteractorSpec: QuickSpec { expect(connections?.oauth2[0].name) == "facebook1" expect(connections?.oauth2[1].name) == "facebook2" } - + it("should load database & oauth2 connection") { stub(isCDN(forClientId: clientId)) { _ in return Auth0Stubs.strategiesFromCDN([ mockStrategy("auth0", connections: [mockDatabaseConnection(databaseConnection)]), mockStrategy("facebook", connections: [mockOAuth2("facebook")]) - ]) + ]) } loader.load(callback) expect(connections?.database?.name).toEventually(equal(databaseConnection)) expect(connections?.oauth2).toEventually(haveCount(1)) } + + // MARK: Enterprise + + it("should load enterprise connections") { + stub(isCDN(forClientId: clientId)) { _ in return Auth0Stubs.strategiesFromCDN([ + mockStrategy("ad", connections: [ + mockEntepriseConnection("testAD", domain: ["test.com"]), + mockEntepriseConnection("fakeAD", domain: ["fake.com"])] + )]) } + loader.load(callback) + expect(connections?.enterprise).toEventuallyNot(beNil()) + expect(connections?.enterprise.count).toEventually(be(2)) + expect(connections?.enterprise[0].name) == "testAD" + expect(connections?.enterprise[1].name) == "fakeAD" + } + + it("should load database & enterprise connections") { + stub(isCDN(forClientId: clientId)) { _ in return Auth0Stubs.strategiesFromCDN([ + mockStrategy("auth0", connections: [mockDatabaseConnection(databaseConnection)]), + mockStrategy("ad", connections: [ + mockEntepriseConnection("testAD", domain: ["test.com"]), + mockEntepriseConnection("fakeAD", domain: ["fake.com"])] + )]) } + loader.load(callback) + expect(connections?.database?.name).toEventually(equal(databaseConnection)) + + expect(connections?.enterprise).toEventuallyNot(beNil()) + expect(connections?.enterprise.count).toEventually(be(2)) + expect(connections?.enterprise[0].name) == "testAD" + expect(connections?.enterprise[1].name) == "fakeAD" + } + + it("should load enterprise & social connections") { + stub(isCDN(forClientId: clientId)) { _ in return Auth0Stubs.strategiesFromCDN([ + mockStrategy("facebook", connections: [ + mockOAuth2("facebook1"), + mockOAuth2("facebook2")]), + mockStrategy("ad", connections: [ + mockEntepriseConnection("testAD", domain: ["test.com"]), + mockEntepriseConnection("fakeAD", domain: ["fake.com"])] + )]) } + loader.load(callback) + expect(connections?.oauth2).toEventuallyNot(beNil()) + expect(connections?.oauth2.count).toEventually(be(2)) + expect(connections?.oauth2[0].name) == "facebook1" + expect(connections?.oauth2[1].name) == "facebook2" + expect(connections?.enterprise).toEventuallyNot(beNil()) + expect(connections?.enterprise.count).toEventually(be(2)) + expect(connections?.enterprise[0].name) == "testAD" + expect(connections?.enterprise[1].name) == "fakeAD" + } + + it("should load enterprise, database & social connections") { + stub(isCDN(forClientId: clientId)) { _ in return Auth0Stubs.strategiesFromCDN([ + mockStrategy("auth0", connections: [mockDatabaseConnection(databaseConnection)]), + mockStrategy("facebook", connections: [ + mockOAuth2("facebook1"), + mockOAuth2("facebook2")]), + mockStrategy("ad", connections: [ + mockEntepriseConnection("testAD", domain: ["test.com"]), + mockEntepriseConnection("fakeAD", domain: ["fake.com"])] + )]) } + loader.load(callback) + expect(connections?.database?.name).toEventually(equal(databaseConnection)) + + expect(connections?.oauth2).toEventuallyNot(beNil()) + expect(connections?.oauth2.count).toEventually(be(2)) + expect(connections?.oauth2[0].name) == "facebook1" + expect(connections?.oauth2[1].name) == "facebook2" + + expect(connections?.enterprise).toEventuallyNot(beNil()) + expect(connections?.enterprise.count).toEventually(be(2)) + expect(connections?.enterprise[0].name) == "testAD" + expect(connections?.enterprise[1].name) == "fakeAD" + } + } - + } - + } private func mockStrategy(name: String, connections: [JSONObject]) -> JSONObject { @@ -235,3 +318,8 @@ private func mockDatabaseConnection(name: String, requiresUsername: Bool? = nil, json["validation"] = validation return json } + +private func mockEntepriseConnection(name: String, domain: [String] ) -> JSONObject { + let json: JSONObject = ["name" : name, "domain" : domain.first!, "domain_aliases" : domain] + return json +} diff --git a/LockTests/Interactors/EnterpriseDomainInteractorSpec.swift b/LockTests/Interactors/EnterpriseDomainInteractorSpec.swift new file mode 100644 index 000000000..df8fb376e --- /dev/null +++ b/LockTests/Interactors/EnterpriseDomainInteractorSpec.swift @@ -0,0 +1,192 @@ +// DatabasePasswordInteractorSpec.swift +// +// Copyright (c) 2016 Auth0 (http://auth0.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Quick +import Nimble +import OHHTTPStubs +import Auth0 + +@testable import Lock + +private struct Enterprise { + struct Connection { + struct Single { + static let name = "TestAD" + static let domain = ["test.com"] + static let validDomain = "user@test.com" + } + struct MultiDomain { + static let name = "TestAD" + static let domain = ["test.com","pepe.com"] + static let validDomain = ["user@test.com", "user@pepe.com"] + } + } +} + +class EnterpriseDomainInteractorSpec: QuickSpec { + + override func spec() { + + var authentication: Auth0OAuth2Interactor! + var webAuth: MockWebAuth! + var credentials: Credentials? + var connections: OfflineConnections! + var enterprise: EnterpriseDomainInteractor! + + beforeEach { + connections = OfflineConnections() + connections.enterprise(name: Enterprise.Connection.Single.name, domains: Enterprise.Connection.Single.domain) + + credentials = nil + webAuth = MockWebAuth() + authentication = Auth0OAuth2Interactor(webAuth: webAuth, onCredentials: {credentials = $0}, options: LockOptions()) + enterprise = EnterpriseDomainInteractor(connections: connections.enterprise, authentication: authentication) + } + + afterEach { + Auth0Stubs.cleanAll() + Auth0Stubs.failUnknown() + } + + describe("init") { + + it("should have an entperise object") { + expect(enterprise).toNot(beNil()) + } + + } + + describe("updateEmail") { + + context("connection with no domain") { + + beforeEach { + connections = OfflineConnections() + connections.enterprise(name: Enterprise.Connection.Single.name, domains: []) + enterprise = EnterpriseDomainInteractor(connections: connections.enterprise, authentication: authentication) + } + + it("should raise no error but no connection provided") { + expect{ try enterprise.updateEmail("user@domainnotmatched.com") }.toNot(throwError()) + expect(enterprise.connection).to(beNil()) + } + + } + + context("connection with one domain") { + + + beforeEach { + connections = OfflineConnections() + connections.enterprise(name: Enterprise.Connection.Single.name, domains: Enterprise.Connection.Single.domain) + enterprise = EnterpriseDomainInteractor(connections: connections.enterprise, authentication: authentication) + } + + it("should match email domain") { + expect{ try enterprise.updateEmail(Enterprise.Connection.Single.validDomain) }.toNot(throwError()) + expect(enterprise.email) == Enterprise.Connection.Single.validDomain + } + + it("should match email domain and provide enteprise connection") { + try! enterprise.updateEmail(Enterprise.Connection.Single.validDomain) + expect(enterprise.connection?.name) == Enterprise.Connection.Single.name + } + + + it("should not match connection with uknown domain") { + try! enterprise.updateEmail("user@domainnotmatched.com") + expect(enterprise.connection).to(beNil()) + } + + it("should raise error if email is nil") { + expect{ try enterprise.updateEmail(nil)}.to(throwError()) + } + + it("should not match a connection with nil email") { + expect{ try enterprise.updateEmail(nil)}.to(throwError()) + expect(enterprise.connection).to(beNil()) + } + } + + context("connection with multiple domains") { + + beforeEach { + connections = OfflineConnections() + connections.enterprise(name: Enterprise.Connection.MultiDomain.name, domains: Enterprise.Connection.MultiDomain.domain) + + enterprise = EnterpriseDomainInteractor(connections: connections.enterprise, authentication: authentication) + } + + it("should match first email domain and provide enteprise connection") { + try! enterprise.updateEmail(Enterprise.Connection.MultiDomain.validDomain.first) + expect(enterprise.connection?.name) == Enterprise.Connection.MultiDomain.name + } + + it("should match second email domain and provide enteprise connection") { + try! enterprise.updateEmail(Enterprise.Connection.MultiDomain.validDomain.last) + expect(enterprise.connection?.name) == Enterprise.Connection.MultiDomain.name + } + } + + } + + describe("login") { + + var error: OAuth2AuthenticatableError? + + beforeEach { + error = nil + + connections = OfflineConnections() + connections.enterprise(name: Enterprise.Connection.Single.name, domains: Enterprise.Connection.Single.domain) + enterprise = EnterpriseDomainInteractor(connections: connections.enterprise, authentication: authentication) + } + + it("should fail to launch on no valid connection") { + + try! enterprise.updateEmail("user@domainnotmatched.com") + enterprise.login() { error = $0 } + expect(error).toEventually(equal(OAuth2AuthenticatableError.NoConnectionAvailable)) + } + + it("should not yield error on success") { + + webAuth.result = { return .Success(result: mockCredentials()) } + + try! enterprise.updateEmail(Enterprise.Connection.Single.validDomain) + enterprise.login() { error = $0 } + expect(error).toEventually(beNil()) + } + + it("should call credentials callback") { + let expected = mockCredentials() + webAuth.result = { return .Success(result: expected) } + + try! enterprise.updateEmail(Enterprise.Connection.Single.validDomain) + enterprise.login() { error = $0 } + expect(credentials).toEventually(equal(expected)) + } + + + } + } +} diff --git a/LockTests/Presenters/AuthPresenterSpec.swift b/LockTests/Presenters/AuthPresenterSpec.swift index e11993836..3a3800ffd 100644 --- a/LockTests/Presenters/AuthPresenterSpec.swift +++ b/LockTests/Presenters/AuthPresenterSpec.swift @@ -61,25 +61,25 @@ class AuthPresenterSpec: QuickSpec { } it("should return view with expanded mode for single connection") { - let connections = OfflineConnections(databases: [], oauth2: mockConnections(count: 1)) + let connections = OfflineConnections(databases: [], oauth2: mockConnections(count: 1), enterprise: []) presenter = AuthPresenter(connections: connections, interactor: interactor, customStyle: [:]) expect(presenter.newViewToEmbed(withInsets: UIEdgeInsetsZero).mode).to(beExpandedMode()) } it("should return view with expanded mode and signup flag") { - let connections = OfflineConnections(databases: [], oauth2: mockConnections(count: 1)) + let connections = OfflineConnections(databases: [], oauth2: mockConnections(count: 1), enterprise: []) presenter = AuthPresenter(connections: connections, interactor: interactor, customStyle: [:]) expect(presenter.newViewToEmbed(withInsets: UIEdgeInsetsZero, isLogin: false).mode).to(beExpandedMode(isLogin: false)) } it("should return view with expanded mode for two connections") { - let connections = OfflineConnections(databases: [], oauth2: mockConnections(count: 2)) + let connections = OfflineConnections(databases: [], oauth2: mockConnections(count: 2), enterprise: []) presenter = AuthPresenter(connections: connections, interactor: interactor, customStyle: [:]) expect(presenter.newViewToEmbed(withInsets: UIEdgeInsetsZero).mode).to(beExpandedMode()) } it("should return view with compact mode for more than three connecitons") { - let connections = OfflineConnections(databases: [], oauth2: mockConnections(count: Int(arc4random_uniform(10)) + 3)) + let connections = OfflineConnections(databases: [], oauth2: mockConnections(count: Int(arc4random_uniform(10)) + 3), enterprise: []) presenter = AuthPresenter(connections: connections, interactor: interactor, customStyle: [:]) expect(presenter.newViewToEmbed(withInsets: UIEdgeInsetsZero).mode).to(beCompactMode()) } diff --git a/LockTests/Presenters/EnterpriseDomainPresenterSpec.swift b/LockTests/Presenters/EnterpriseDomainPresenterSpec.swift new file mode 100644 index 000000000..c78f2371e --- /dev/null +++ b/LockTests/Presenters/EnterpriseDomainPresenterSpec.swift @@ -0,0 +1,161 @@ +// EnterpriseDomainPresenterSpec.swift +// +// Copyright (c) 2016 Auth0 (http://auth0.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Quick +import Nimble + +@testable import Lock + +class EnterpriseDomainPresenterSpec: QuickSpec { + + override func spec() { + + var interactor: EnterpriseDomainInteractor! + var presenter: EnterpriseDomainPresenter! + var view: EnterpriseDomainView! + var messagePresenter: MockMessagePresenter! + var connections: OfflineConnections! + var oauth2: MockOAuth2! + + beforeEach { + messagePresenter = MockMessagePresenter() + oauth2 = MockOAuth2() + connections = OfflineConnections() + connections.enterprise(name: "testAD", domains: ["test.com"]) + + interactor = EnterpriseDomainInteractor(connections: connections.enterprise, authentication: oauth2) + + presenter = EnterpriseDomainPresenter(interactor: interactor) + presenter.messagePresenter = messagePresenter + + view = presenter.view as! EnterpriseDomainView + } + + describe("email input validation") { + + it("should use valid email") { + interactor.email = email + interactor.validEmail = true + presenter = EnterpriseDomainPresenter(interactor: interactor) + + let view = (presenter.view as! EnterpriseDomainView).form as! EnterpriseView + expect(view.value).to(equal(email)) + } + + it("should not use invalid email") { + interactor.email = email + interactor.validEmail = false + presenter = EnterpriseDomainPresenter(interactor: interactor) + + let view = (presenter.view as! EnterpriseDomainView).form as! EnterpriseView + expect(view.value).toNot(equal(email)) + } + } + + + + describe("user input") { + + it("email should update with valid email") { + let input = mockInput(.Email, value: "valid@email.com") + view.form?.onValueChange(input) + expect(presenter.interactor.email).to(equal("valid@email.com")) + } + + it("email should be invalid when nil") { + let input = mockInput(.Email, value: nil) + view.form?.onValueChange(input) + expect(presenter.interactor.validEmail).to(equal(false)) + } + + it("email should be invalid when garbage") { + let input = mockInput(.Email, value: " ") + view.form?.onValueChange(input) + expect(presenter.interactor.validEmail).to(equal(false)) + } + + it("connection should match with valid domain") { + let input = mockInput(.Email, value: "valid@test.com") + view.form?.onValueChange(input) + expect(presenter.interactor.connection).toNot(beNil()) + expect(presenter.interactor.connection?.name).to(equal("testAD")) + } + + it("connection should not match with an invalid domain") { + let input = mockInput(.Email, value: "email@nomatchdomain.com") + view.form?.onValueChange(input) + expect(presenter.interactor.connection).to(beNil()) + } + + it("should hide the field error if value is valid") { + let input = mockInput(.Email, value: email) + view.form?.onValueChange(input) + expect(input.valid).to(equal(true)) + } + + it("should show field error if value is invalid") { + let input = mockInput(.Email, value: "invalid") + view.form?.onValueChange(input) + expect(input.valid).to(equal(false)) + } + + } + + + describe("login action") { + + it("should not trigger action with nil button") { + let input = mockInput(.Email, value: "invalid") + input.returnKey = .Done + view.primaryButton = nil + view.form?.onReturn(input) + expect(messagePresenter.message).toEventually(beNil()) + expect(messagePresenter.error).toEventually(beNil()) + } + + + it("should fail when no connection is matched") { + presenter.interactor.connection = nil + view.primaryButton?.onPress(view.primaryButton!) + expect(messagePresenter.error).toEventually(beError(error: OAuth2AuthenticatableError.NoConnectionAvailable)) + } + + it("should show yield oauth2 error on failure") { + presenter.interactor.connection = EnterpriseConnection(name: "ad", domains: ["auth0.com"]) + oauth2.onLogin = { return OAuth2AuthenticatableError.CouldNotAuthenticate } + view.primaryButton?.onPress(view.primaryButton!) + expect(messagePresenter.error).toEventually(beError(error: OAuth2AuthenticatableError.CouldNotAuthenticate)) + } + + it("should show no error on success") { + let input = mockInput(.Email, value: "user@test.com") + view.form?.onValueChange(input) + view.primaryButton?.onPress(view.primaryButton!) + expect(messagePresenter.error).toEventually(beNil()) + } + + + } + } + +} +