From 69d034eff5c37b02ba71ef80ca88d58d997d3221 Mon Sep 17 00:00:00 2001 From: Martin Walsh Date: Thu, 29 Dec 2016 10:01:00 +0000 Subject: [PATCH] Feature to disable log in after sign up Generic notification presenter added Tests added --- Lock.xcodeproj/project.pbxproj | 12 + Lock/DatabaseAuthenticatableError.swift | 1 + Lock/DatabaseInteractor.swift | 6 +- Lock/DatabasePresenter.swift | 6 +- .../ic_email_sent.imageset/Contents.json | 15 + .../ic_email_sent.imageset/ic_email_sent.pdf | Bin 0 -> 4683 bytes Lock/LockOptions.swift | 1 + Lock/NotificationPresenter.swift | 39 ++ Lock/NotificationStatus.swift | 27 ++ Lock/NotificationView.swift | 69 ++++ Lock/OptionBuildable.swift | 3 + Lock/Options.swift | 1 + Lock/Router.swift | 8 + Lock/Routes.swift | 3 + .../Interactors/DatabaseInteractorSpec.swift | 385 ++++++++++-------- LockTests/Router/RouterSpec.swift | 6 + 16 files changed, 404 insertions(+), 178 deletions(-) create mode 100644 Lock/Lock.xcassets/ic_email_sent.imageset/Contents.json create mode 100644 Lock/Lock.xcassets/ic_email_sent.imageset/ic_email_sent.pdf create mode 100644 Lock/NotificationPresenter.swift create mode 100644 Lock/NotificationStatus.swift create mode 100644 Lock/NotificationView.swift diff --git a/Lock.xcodeproj/project.pbxproj b/Lock.xcodeproj/project.pbxproj index 254f76f4f..2ac99350a 100644 --- a/Lock.xcodeproj/project.pbxproj +++ b/Lock.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + 5B011CDE1E13E16500543F12 /* NotificationStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B011CDD1E13E16500543F12 /* NotificationStatus.swift */; }; + 5B011CE01E13E3DB00543F12 /* NotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B011CDF1E13E3DB00543F12 /* NotificationPresenter.swift */; }; + 5B011CE21E13F2EF00543F12 /* NotificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B011CE11E13F2EF00543F12 /* NotificationView.swift */; }; 5B09717C1DC8F229003AA88F /* EnterpriseDomain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B09717B1DC8F229003AA88F /* EnterpriseDomain.swift */; }; 5B09717E1DC8F292003AA88F /* EnterpriseActiveAuthInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B09717D1DC8F292003AA88F /* EnterpriseActiveAuthInteractor.swift */; }; 5B0971801DC8F5C4003AA88F /* EnterpriseDomainPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B09717F1DC8F5C4003AA88F /* EnterpriseDomainPresenter.swift */; }; @@ -190,6 +193,9 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 5B011CDD1E13E16500543F12 /* NotificationStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationStatus.swift; sourceTree = ""; }; + 5B011CDF1E13E3DB00543F12 /* NotificationPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationPresenter.swift; sourceTree = ""; }; + 5B011CE11E13F2EF00543F12 /* NotificationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationView.swift; sourceTree = ""; }; 5B09717B1DC8F229003AA88F /* EnterpriseDomain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = EnterpriseDomain.swift; path = Lock/EnterpriseDomain.swift; sourceTree = SOURCE_ROOT; }; 5B09717D1DC8F292003AA88F /* EnterpriseActiveAuthInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = EnterpriseActiveAuthInteractor.swift; path = Lock/EnterpriseActiveAuthInteractor.swift; sourceTree = SOURCE_ROOT; }; 5B09717F1DC8F5C4003AA88F /* EnterpriseDomainPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = EnterpriseDomainPresenter.swift; path = Lock/EnterpriseDomainPresenter.swift; sourceTree = SOURCE_ROOT; }; @@ -437,6 +443,7 @@ children = ( 5BB4A7C01DF9A38E008E8C37 /* DatabaseView.swift */, 5F1C49921D8360DF005B74FC /* LoadingView.swift */, + 5B011CE11E13F2EF00543F12 /* NotificationView.swift */, 5F73CDD31D3073BE00D8D8D1 /* DatabaseForgotPasswordView.swift */, 5B0971811DC8FAC5003AA88F /* EnterpriseDomainView.swift */, 5B4DE0181DD670F7004C8AC2 /* EnterpriseActiveAuthView.swift */, @@ -536,6 +543,7 @@ children = ( 5FEEE8161DB6AC6C00B4DFED /* PasswordPolicy */, 5F2496B71D665AC500A1C6E2 /* UserAttribute.swift */, + 5B011CDD1E13E16500543F12 /* NotificationStatus.swift */, 5FBE5CB71D3D8F030038536D /* User.swift */, 5F57DFC51D4F79DD00C54DA8 /* AuthStyle.swift */, 5F1C498D1D8360AA005B74FC /* Style.swift */, @@ -564,6 +572,7 @@ 5F1C498F1D8360BF005B74FC /* ConnectionLoadingPresenter.swift */, 5B09717F1DC8F5C4003AA88F /* EnterpriseDomainPresenter.swift */, 5F73CDD51D30790500D8D8D1 /* DatabaseForgotPasswordPresenter.swift */, + 5B011CDF1E13E3DB00543F12 /* NotificationPresenter.swift */, 5B4DE0161DD67064004C8AC2 /* EnterpriseActiveAuthPresenter.swift */, 5FC434891D1DF82A005188BC /* DatabasePresenter.swift */, 5F73CDCF1D30250900D8D8D1 /* MessagePresenter.swift */, @@ -939,6 +948,7 @@ 5F2496B31D665A5600A1C6E2 /* DatabaseUserCreator.swift in Sources */, 5F1C499B1D836190005B74FC /* CustomTextField.swift in Sources */, 5BB4A7C11DF9A38E008E8C37 /* DatabaseView.swift in Sources */, + 5B011CE01E13E3DB00543F12 /* NotificationPresenter.swift in Sources */, 5F70F1E71D790773004698DA /* OptionBuildable.swift in Sources */, 5FBE5CB81D3D8F030038536D /* User.swift in Sources */, 5F99AA861D1B0BF100D27842 /* Resources.swift in Sources */, @@ -953,6 +963,7 @@ 5FC4348C1D1DFC5A005188BC /* Form.swift in Sources */, 5F73CDD01D30250900D8D8D1 /* MessagePresenter.swift in Sources */, 5F70F1E51D790735004698DA /* Options.swift in Sources */, + 5B011CE21E13F2EF00543F12 /* NotificationView.swift in Sources */, 5FDB41D01D2C95B100166B67 /* MessageView.swift in Sources */, 5F2496B61D665AA800A1C6E2 /* InputValidationError.swift in Sources */, 5FFC54FE1D37E3F700579581 /* Routes.swift in Sources */, @@ -999,6 +1010,7 @@ 5F73CDD41D3073BE00D8D8D1 /* DatabaseForgotPasswordView.swift in Sources */, 5F99AA961D1C4AF400D27842 /* CredentialView.swift in Sources */, 5F1C498E1D8360AA005B74FC /* Style.swift in Sources */, + 5B011CDE1E13E16500543F12 /* NotificationStatus.swift in Sources */, 5F73CDDA1D30957900D8D8D1 /* DatabasePasswordInteractor.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Lock/DatabaseAuthenticatableError.swift b/Lock/DatabaseAuthenticatableError.swift index 99877d7c9..c6ebd9e2a 100644 --- a/Lock/DatabaseAuthenticatableError.swift +++ b/Lock/DatabaseAuthenticatableError.swift @@ -32,6 +32,7 @@ enum DatabaseAuthenticatableError: Error, LocalizableError { case tooManyAttempts case multifactorRequired case multifactorInvalid + case noLoginAfterSignup var localizableMessage: String { switch self { diff --git a/Lock/DatabaseInteractor.swift b/Lock/DatabaseInteractor.swift index 4c042d18e..c85765b64 100644 --- a/Lock/DatabaseInteractor.swift +++ b/Lock/DatabaseInteractor.swift @@ -140,7 +140,11 @@ struct DatabaseInteractor: DatabaseAuthenticatable, DatabaseUserCreator, Loggabl .start { switch $0 { case .success: - login.start { self.handle(result: $0, callback: { callback(nil, $0) }) } + if self.options.loginAfterSignup { + login.start { self.handle(result: $0, callback: { callback(nil, $0) }) } + } else { + callback(nil,.noLoginAfterSignup) + } case .failure(let cause as AuthenticationError) where cause.isPasswordNotStrongEnough: callback(.passwordTooWeak, nil) case .failure(let cause as AuthenticationError) where cause.isPasswordAlreadyUsed: diff --git a/Lock/DatabasePresenter.swift b/Lock/DatabasePresenter.swift index e6a8623d4..2fcde0ba5 100644 --- a/Lock/DatabasePresenter.swift +++ b/Lock/DatabasePresenter.swift @@ -160,7 +160,6 @@ class DatabasePresenter: Presentable, Loggable { interactor.create { createError, loginError in Queue.main.async { button.inProgress = false - guard createError != nil || loginError != nil else { self.logger.debug("Logged in!") return @@ -169,7 +168,10 @@ class DatabasePresenter: Presentable, Loggable { self.navigator.navigate(.multifactor) return } - + if let error = loginError, case .noLoginAfterSignup = error { + self.navigator.navigate(.notification(status: .signedup)) + return + } let error: LocalizableError = createError ?? loginError! form?.needsToUpdateState() self.messagePresenter?.showError(error) diff --git a/Lock/Lock.xcassets/ic_email_sent.imageset/Contents.json b/Lock/Lock.xcassets/ic_email_sent.imageset/Contents.json new file mode 100644 index 000000000..79e1ad4b0 --- /dev/null +++ b/Lock/Lock.xcassets/ic_email_sent.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_email_sent.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "original" + } +} \ No newline at end of file diff --git a/Lock/Lock.xcassets/ic_email_sent.imageset/ic_email_sent.pdf b/Lock/Lock.xcassets/ic_email_sent.imageset/ic_email_sent.pdf new file mode 100644 index 0000000000000000000000000000000000000000..5e3f1dcf698475a5532330e12a08870af5c9f878 GIT binary patch literal 4683 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 D_1PrI literal 0 HcmV?d00001 diff --git a/Lock/LockOptions.swift b/Lock/LockOptions.swift index 918fb41ed..1d41e8ef7 100644 --- a/Lock/LockOptions.swift +++ b/Lock/LockOptions.swift @@ -36,6 +36,7 @@ struct LockOptions: OptionBuildable { var initialScreen: DatabaseScreen = .login var usernameStyle: DatabaseIdentifierStyle = [.Username, .Email] var customSignupFields: [CustomTextField] = [] + var loginAfterSignup: Bool = true // Enterprise var activeDirectoryEmailAsUsername: Bool = false diff --git a/Lock/NotificationPresenter.swift b/Lock/NotificationPresenter.swift new file mode 100644 index 000000000..feee9e5fa --- /dev/null +++ b/Lock/NotificationPresenter.swift @@ -0,0 +1,39 @@ +// NotificationPresenter.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 NotificationPresenter: Presentable, Loggable { + + var customLogger: Logger? + var status: NotificationStatus + var messagePresenter: MessagePresenter? + + init(status: NotificationStatus) { + self.status = status + } + + var view: View { + let view = NotificationView(withStatus: self.status) + return view + } +} diff --git a/Lock/NotificationStatus.swift b/Lock/NotificationStatus.swift new file mode 100644 index 000000000..3c879c4cd --- /dev/null +++ b/Lock/NotificationStatus.swift @@ -0,0 +1,27 @@ +// NotificationStatus.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 + +enum NotificationStatus { + case signedup +} diff --git a/Lock/NotificationView.swift b/Lock/NotificationView.swift new file mode 100644 index 000000000..68a6ca1b5 --- /dev/null +++ b/Lock/NotificationView.swift @@ -0,0 +1,69 @@ +// NotificationView.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 NotificationView: UIView, View { + + init(withStatus status: NotificationStatus) { + super.init(frame: CGRect.zero) + + let center = UILayoutGuide() + let imageView = UIImageView() + let messageLabel = UILabel() + + self.addSubview(imageView) + self.addSubview(messageLabel) + self.addLayoutGuide(center) + + constraintEqual(anchor: center.leftAnchor, toAnchor: self.leftAnchor) + constraintEqual(anchor: center.topAnchor, toAnchor: self.topAnchor) + constraintEqual(anchor: center.rightAnchor, toAnchor: self.rightAnchor) + constraintEqual(anchor: center.bottomAnchor, toAnchor: self.bottomAnchor) + + constraintEqual(anchor: imageView.centerYAnchor, toAnchor: center.centerYAnchor, constant: -20) + constraintEqual(anchor: imageView.centerXAnchor, toAnchor: center.centerXAnchor) + imageView.translatesAutoresizingMaskIntoConstraints = false + + constraintEqual(anchor: imageView.bottomAnchor, toAnchor: messageLabel.topAnchor, constant: -20) + constraintEqual(anchor: center.centerXAnchor, toAnchor: messageLabel.centerXAnchor) + messageLabel.translatesAutoresizingMaskIntoConstraints = false + + messageLabel.numberOfLines = 0 + messageLabel.textAlignment = .center + messageLabel.font = regularSystemFont(size: 15) + + switch(status) { + case .signedup: + messageLabel.text = "Thanks for signing up.".i18n(key: "com.auth0.lock.notification.signup", comment: "Signed Up") + imageView.image = LazyImage(name: "ic_email_sent", bundle: bundleForLock()).image() + } + + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func apply(style: Style) { + } +} diff --git a/Lock/OptionBuildable.swift b/Lock/OptionBuildable.swift index fd75f5573..fb3ab1bd2 100644 --- a/Lock/OptionBuildable.swift +++ b/Lock/OptionBuildable.swift @@ -67,6 +67,9 @@ public protocol OptionBuildable: Options { /// Additional fields showed for Database Sign Up. By default the list is empty var customSignupFields: [CustomTextField] { get set } + /// Automatically log user in after sign up. By default true + var loginAfterSignup: Bool { get set } + /// Should enterprise credential auth require email instead of username. By default is false var activeDirectoryEmailAsUsername: Bool { get set } diff --git a/Lock/Options.swift b/Lock/Options.swift index 4e98c2621..ba2c99ed2 100644 --- a/Lock/Options.swift +++ b/Lock/Options.swift @@ -38,6 +38,7 @@ public protocol Options { var initialScreen: DatabaseScreen { get } var usernameStyle: DatabaseIdentifierStyle { get } var customSignupFields: [CustomTextField] { get } + var loginAfterSignup: Bool { get } // Enterprise var activeDirectoryEmailAsUsername: Bool { get } diff --git a/Lock/Router.swift b/Lock/Router.swift index 94cde046d..6e473e84c 100644 --- a/Lock/Router.swift +++ b/Lock/Router.swift @@ -160,6 +160,12 @@ class Router: Navigable { return presenter } + func notification(_ status: NotificationStatus) -> Presentable? { + let presenter = NotificationPresenter(status: status) + presenter.customLogger = self.lock.logger + return presenter + } + var showBack: Bool { guard let routes = self.controller?.routes else { return false } return !routes.history.isEmpty @@ -187,6 +193,8 @@ class Router: Navigable { presentable = self.multifactor case .enterpriseActiveAuth(let connection): presentable = self.EnterpriseActiveAuth(connection) + case .notification(let status): + presentable = self.notification(status) default: self.lock.logger.warn("Ignoring navigation \(route)") return diff --git a/Lock/Routes.swift b/Lock/Routes.swift index ee4027f49..95d2946bf 100644 --- a/Lock/Routes.swift +++ b/Lock/Routes.swift @@ -49,6 +49,7 @@ enum Route: Equatable { case forgotPassword case multifactor case enterpriseActiveAuth(connection: EnterpriseConnection) + case notification(status: NotificationStatus) } func == (lhs: Route, rhs: Route) -> Bool { @@ -57,6 +58,8 @@ func == (lhs: Route, rhs: Route) -> Bool { return true case (.enterpriseActiveAuth(let lhsConnection), .enterpriseActiveAuth(let rhsConnection)): return lhsConnection.name == rhsConnection.name + case (.notification(let lhsStatus), .notification(let rhsStatus)): + return lhsStatus == rhsStatus default: return false } diff --git a/LockTests/Interactors/DatabaseInteractorSpec.swift b/LockTests/Interactors/DatabaseInteractorSpec.swift index 22b89921d..3fd1887c4 100644 --- a/LockTests/Interactors/DatabaseInteractorSpec.swift +++ b/LockTests/Interactors/DatabaseInteractorSpec.swift @@ -547,206 +547,241 @@ class DatabaseInteractorSpec: QuickSpec { describe("signup") { - it("should yield no error on success") { - stub(condition: databaseSignUp(email: email, username: username, password: password, connection: connection)) { _ in return Auth0Stubs.createdUser(email) } - stub(condition: databaseLogin(identifier: email, password: password, connection: connection)) { _ in return Auth0Stubs.authentication() } - try! database.update(.email, value: email) - try! database.update(.username, value: username) - try! database.update(.password(enforcePolicy: false), value: password) - waitUntil(timeout: 2) { done in - database.create { create, login in - expect(create).to(beNil()) - expect(login).to(beNil()) - done() - } - } - } + context("Auto log in after sign up") { - it("should not send username") { - let username = "AN INVALID USERNAME" - database = DatabaseInteractor(connection: DatabaseConnection(name: connection, requiresUsername: false), authentication: authentication, user: user, options: LockOptions(), callback: { _ in }) - stub(condition: databaseSignUp(email: email, password: password, connection: connection) && !hasEntry(key: "username", value: username)) { _ in return Auth0Stubs.createdUser(email) } - stub(condition: databaseLogin(identifier: email, password: password, connection: connection)) { _ in return Auth0Stubs.authentication() } - try! database.update(.email, value: email) - let _ = try? database.update(.username, value: username) - try! database.update(.password(enforcePolicy: false), value: password) - waitUntil(timeout: 2) { done in - database.create { create, login in - expect(create).to(beNil()) - expect(login).to(beNil()) - done() - } - } - } + var options = LockOptions() - it("should indicate that mfa is required") { - stub(condition: databaseSignUp(email: email, username: username, password: password, connection: connection)) { _ in return Auth0Stubs.createdUser(email) } - stub(condition: databaseLogin(identifier: email, password: password, connection: connection)) { _ in return Auth0Stubs.failure("a0.mfa_required") } - try! database.update(.email, value: email) - try! database.update(.username, value: username) - try! database.update(.password(enforcePolicy: false), value: password) - waitUntil(timeout: 2) { done in - database.create { create, login in - expect(create).to(beNil()) - expect(login) == .multifactorRequired - done() - } + beforeEach { + options.loginAfterSignup = true + let db = DatabaseConnection(name: connection, requiresUsername: true, usernameValidator: UsernameValidator(withLength: 1...15, characterSet: UsernameValidator.auth0), passwordValidator: PasswordPolicyValidator(policy: .good)) + database = DatabaseInteractor(connection: db, authentication: authentication, user: user, options: options, callback: { _ in }) + } + + it("should yield no error on success") { + stub(condition: databaseSignUp(email: email, username: username, password: password, connection: connection)) { _ in return Auth0Stubs.createdUser(email) } + stub(condition: databaseLogin(identifier: email, password: password, connection: connection)) { _ in return Auth0Stubs.authentication() } + try! database.update(.email, value: email) + try! database.update(.username, value: username) + try! database.update(.password(enforcePolicy: false), value: password) + waitUntil(timeout: 2) { done in + database.create { create, login in + expect(create).to(beNil()) + expect(login).to(beNil()) + done() + } + } + } + + it("should not send username") { + let username = "AN INVALID USERNAME" + database = DatabaseInteractor(connection: DatabaseConnection(name: connection, requiresUsername: false), authentication: authentication, user: user, options: options, callback: { _ in }) + stub(condition: databaseSignUp(email: email, password: password, connection: connection) && !hasEntry(key: "username", value: username)) { _ in return Auth0Stubs.createdUser(email) } + stub(condition: databaseLogin(identifier: email, password: password, connection: connection)) { _ in return Auth0Stubs.authentication() } + try! database.update(.email, value: email) + let _ = try? database.update(.username, value: username) + try! database.update(.password(enforcePolicy: false), value: password) + waitUntil(timeout: 2) { done in + database.create { create, login in + expect(create).to(beNil()) + expect(login).to(beNil()) + done() + } + } + } + + it("should indicate that mfa is required") { + stub(condition: databaseSignUp(email: email, username: username, password: password, connection: connection)) { _ in return Auth0Stubs.createdUser(email) } + stub(condition: databaseLogin(identifier: email, password: password, connection: connection)) { _ in return Auth0Stubs.failure("a0.mfa_required") } + try! database.update(.email, value: email) + try! database.update(.username, value: username) + try! database.update(.password(enforcePolicy: false), value: password) + waitUntil(timeout: 2) { done in + database.create { create, login in + expect(create).to(beNil()) + expect(login) == .multifactorRequired + done() + } + } + } + + it("should yield error on login failure") { + stub(condition: databaseSignUp(email: email, username: username, password: password, connection: connection)) { _ in return Auth0Stubs.createdUser(email) } + stub(condition: databaseLogin(identifier: email, password: password, connection: connection)) { _ in return Auth0Stubs.failure() } + try! database.update(.email, value: email) + try! database.update(.username, value: username) + try! database.update(.password(enforcePolicy: false), value: password) + waitUntil(timeout: 2) { done in + database.create { create, login in + expect(create).to(beNil()) + expect(login) == .couldNotLogin + done() + } + } + } + + it("should yield error on signup failure") { + stub(condition: databaseSignUp(email: email, username: username, password: password, connection: connection)) { _ in return Auth0Stubs.failure() } + try! database.update(.email, value: email) + try! database.update(.username, value: username) + try! database.update(.password(enforcePolicy: false), value: password) + waitUntil(timeout: 2) { done in + database.create { create, login in + expect(create) == .couldNotCreateUser + done() + } + } + } + + it("should yield invalid password") { + stub(condition: databaseSignUp(email: email, username: username, password: password, connection: connection)) { _ in return Auth0Stubs.failure("invalid_password") } + try! database.update(.email, value: email) + try! database.update(.username, value: username) + try! database.update(.password(enforcePolicy: false), value: password) + waitUntil(timeout: 2) { done in + database.create { create, login in + expect(create) == .passwordInvalid + done() + } + } + } + + it("should yield password too weak") { + stub(condition: databaseSignUp(email: email, username: username, password: password, connection: connection)) { _ in return Auth0Stubs.failure("invalid_password", name: "PasswordStrengthError") } + try! database.update(.email, value: email) + try! database.update(.username, value: username) + try! database.update(.password(enforcePolicy: false), value: password) + waitUntil(timeout: 2) { done in + database.create { create, login in + expect(create) == .passwordTooWeak + done() + } + } + } + + it("should yield password already used") { + stub(condition: databaseSignUp(email: email, username: username, password: password, connection: connection)) { _ in return Auth0Stubs.failure("invalid_password", name: "PasswordHistoryError") } + try! database.update(.email, value: email) + try! database.update(.username, value: username) + try! database.update(.password(enforcePolicy: false), value: password) + waitUntil(timeout: 2) { done in + database.create { create, login in + expect(create) == .passwordAlreadyUsed + done() + } + } + } + + it("should yield password too common") { + stub(condition: databaseSignUp(email: email, username: username, password: password, connection: connection)) { _ in return Auth0Stubs.failure("invalid_password", name: "PasswordDictionaryError") } + try! database.update(.email, value: email) + try! database.update(.username, value: username) + try! database.update(.password(enforcePolicy: false), value: password) + waitUntil(timeout: 2) { done in + database.create { create, login in + expect(create) == .passwordTooCommon + done() + } + } } - } - it("should yield error on login failure") { - stub(condition: databaseSignUp(email: email, username: username, password: password, connection: connection)) { _ in return Auth0Stubs.createdUser(email) } - stub(condition: databaseLogin(identifier: email, password: password, connection: connection)) { _ in return Auth0Stubs.failure() } - try! database.update(.email, value: email) - try! database.update(.username, value: username) - try! database.update(.password(enforcePolicy: false), value: password) - waitUntil(timeout: 2) { done in - database.create { create, login in - expect(create).to(beNil()) - expect(login) == .couldNotLogin - done() + it("should yield password has user info") { + stub(condition: databaseSignUp(email: email, username: username, password: password, connection: connection)) { _ in return Auth0Stubs.failure("invalid_password", name: "PasswordNoUserInfoError") } + try! database.update(.email, value: email) + try! database.update(.username, value: username) + try! database.update(.password(enforcePolicy: false), value: password) + waitUntil(timeout: 2) { done in + database.create { create, login in + expect(create) == .passwordHasUserInfo + done() + } } } - } - it("should yield error on signup failure") { - stub(condition: databaseSignUp(email: email, username: username, password: password, connection: connection)) { _ in return Auth0Stubs.failure() } - try! database.update(.email, value: email) - try! database.update(.username, value: username) - try! database.update(.password(enforcePolicy: false), value: password) - waitUntil(timeout: 2) { done in - database.create { create, login in - expect(create) == .couldNotCreateUser - done() + it("should yield error when input is not valid") { + waitUntil(timeout: 2) { done in + database.create { create, login in + expect(create) == .nonValidInput + done() + } } - } - } - - it("should yield invalid password") { - stub(condition: databaseSignUp(email: email, username: username, password: password, connection: connection)) { _ in return Auth0Stubs.failure("invalid_password") } - try! database.update(.email, value: email) - try! database.update(.username, value: username) - try! database.update(.password(enforcePolicy: false), value: password) - waitUntil(timeout: 2) { done in - database.create { create, login in - expect(create) == .passwordInvalid - done() + } + + it("should yield error when username is not valid and required") { + try! database.update(.email, value: email) + try! database.update(.password(enforcePolicy: false), value: password) + waitUntil(timeout: 2) { done in + database.create { create, login in + expect(create) == .nonValidInput + done() + } } } - } - it("should yield password too weak") { - stub(condition: databaseSignUp(email: email, username: username, password: password, connection: connection)) { _ in return Auth0Stubs.failure("invalid_password", name: "PasswordStrengthError") } - try! database.update(.email, value: email) - try! database.update(.username, value: username) - try! database.update(.password(enforcePolicy: false), value: password) - waitUntil(timeout: 2) { done in - database.create { create, login in - expect(create) == .passwordTooWeak - done() - } - } - } - it("should yield password already used") { - stub(condition: databaseSignUp(email: email, username: username, password: password, connection: connection)) { _ in return Auth0Stubs.failure("invalid_password", name: "PasswordHistoryError") } - try! database.update(.email, value: email) - try! database.update(.username, value: username) - try! database.update(.password(enforcePolicy: false), value: password) - waitUntil(timeout: 2) { done in - database.create { create, login in - expect(create) == .passwordAlreadyUsed - done() + it("should send scope on login") { + let scope = "openid email" + options.scope = scope + database = DatabaseInteractor(connection: DatabaseConnection(name: connection, requiresUsername: true), authentication: authentication, user: user, options: options, callback: { _ in }) + stub(condition: databaseSignUp(email: email, username: username, password: password, connection: connection)) { _ in return Auth0Stubs.createdUser(email) } + stub(condition: databaseLogin(identifier: email, password: password, connection: connection) && hasEntry(key: "scope", value: scope)) { _ in return Auth0Stubs.authentication() } + try! database.update(.email, value: email) + try! database.update(.username, value: username) + try! database.update(.password(enforcePolicy: false), value: password) + waitUntil(timeout: 2) { done in + database.create { create, login in + expect(create).to(beNil()) + expect(login).to(beNil()) + done() + } + } + } + + it("should send parameters on login") { + let state = UUID().uuidString + options.parameters = ["state": state as Any] + database = DatabaseInteractor(connection: DatabaseConnection(name: connection, requiresUsername: true), authentication: authentication, user: user, options: options, callback: { _ in }) + stub(condition: databaseSignUp(email: email, username: username, password: password, connection: connection)) { _ in return Auth0Stubs.createdUser(email) } + stub(condition: databaseLogin(identifier: email, password: password, connection: connection) && hasEntry(key: "state", value: state)) { _ in return Auth0Stubs.authentication() } + try! database.update(.email, value: email) + try! database.update(.username, value: username) + try! database.update(.password(enforcePolicy: false), value: password) + waitUntil(timeout: 2) { done in + database.create { create, login in + expect(create).to(beNil()) + expect(login).to(beNil()) + done() + } } } } - it("should yield password too common") { - stub(condition: databaseSignUp(email: email, username: username, password: password, connection: connection)) { _ in return Auth0Stubs.failure("invalid_password", name: "PasswordDictionaryError") } - try! database.update(.email, value: email) - try! database.update(.username, value: username) - try! database.update(.password(enforcePolicy: false), value: password) - waitUntil(timeout: 2) { done in - database.create { create, login in - expect(create) == .passwordTooCommon - done() - } - } - } + context("No auto log in after sign up") { - it("should yield password has user info") { - stub(condition: databaseSignUp(email: email, username: username, password: password, connection: connection)) { _ in return Auth0Stubs.failure("invalid_password", name: "PasswordNoUserInfoError") } - try! database.update(.email, value: email) - try! database.update(.username, value: username) - try! database.update(.password(enforcePolicy: false), value: password) - waitUntil(timeout: 2) { done in - database.create { create, login in - expect(create) == .passwordHasUserInfo - done() - } - } - } + var options = LockOptions() - it("should yield error when input is not valid") { - waitUntil(timeout: 2) { done in - database.create { create, login in - expect(create) == .nonValidInput - done() - } + beforeEach { + options.loginAfterSignup = false + let db = DatabaseConnection(name: connection, requiresUsername: true, usernameValidator: UsernameValidator(withLength: 1...15, characterSet: UsernameValidator.auth0), passwordValidator: PasswordPolicyValidator(policy: .good)) + database = DatabaseInteractor(connection: db, authentication: authentication, user: user, options: options, callback: { _ in }) } - } - it("should yield error when username is not valid and required") { - try! database.update(.email, value: email) - try! database.update(.password(enforcePolicy: false), value: password) - waitUntil(timeout: 2) { done in - database.create { create, login in - expect(create) == .nonValidInput - done() + it("should not auto login after signup") { + stub(condition: databaseSignUp(email: email, username: username, password: password, connection: connection)) { _ in return Auth0Stubs.createdUser(email) } + stub(condition: databaseLogin(identifier: email, password: password, connection: connection)) { _ in return Auth0Stubs.authentication() } + try! database.update(.email, value: email) + try! database.update(.username, value: username) + try! database.update(.password(enforcePolicy: false), value: password) + waitUntil(timeout: 2) { done in + database.create { create, login in + expect(create).to(beNil()) + expect(login) == DatabaseAuthenticatableError.noLoginAfterSignup + done() + } } } - } - - it("should send scope on login") { - let scope = "openid email" - var options = LockOptions() - options.scope = scope - database = DatabaseInteractor(connection: DatabaseConnection(name: connection, requiresUsername: true), authentication: authentication, user: user, options: options, callback: { _ in }) - stub(condition: databaseSignUp(email: email, username: username, password: password, connection: connection)) { _ in return Auth0Stubs.createdUser(email) } - stub(condition: databaseLogin(identifier: email, password: password, connection: connection) && hasEntry(key: "scope", value: scope)) { _ in return Auth0Stubs.authentication() } - try! database.update(.email, value: email) - try! database.update(.username, value: username) - try! database.update(.password(enforcePolicy: false), value: password) - waitUntil(timeout: 2) { done in - database.create { create, login in - expect(create).to(beNil()) - expect(login).to(beNil()) - done() - } - } } - it("should send parameters on login") { - let state = UUID().uuidString - var options = LockOptions() - options.parameters = ["state": state as Any] - database = DatabaseInteractor(connection: DatabaseConnection(name: connection, requiresUsername: true), authentication: authentication, user: user, options: options, callback: { _ in }) - stub(condition: databaseSignUp(email: email, username: username, password: password, connection: connection)) { _ in return Auth0Stubs.createdUser(email) } - stub(condition: databaseLogin(identifier: email, password: password, connection: connection) && hasEntry(key: "state", value: state)) { _ in return Auth0Stubs.authentication() } - try! database.update(.email, value: email) - try! database.update(.username, value: username) - try! database.update(.password(enforcePolicy: false), value: password) - waitUntil(timeout: 2) { done in - database.create { create, login in - expect(create).to(beNil()) - expect(login).to(beNil()) - done() - } - } - } - - } } diff --git a/LockTests/Router/RouterSpec.swift b/LockTests/Router/RouterSpec.swift index 41549dd5b..ea745d283 100644 --- a/LockTests/Router/RouterSpec.swift +++ b/LockTests/Router/RouterSpec.swift @@ -361,6 +361,12 @@ class RouterSpec: QuickSpec { expect(match).to(beTrue()) } + it("Notification should should be equatable with Notification") { + let status = NotificationStatus.signedup + let match = Route.notification(status: status) == Route.notification(status: status) + expect(match).to(beTrue()) + } + it("root should should not be equatable with Multifactor") { let match = Route.root == Route.multifactor expect(match).to(beFalse())