Let the user change e164 after entering one in registration bravo

* Allow changing e164 after entering one + tests

* hook up back to change number from sms code verification screen

* nit

Co-authored-by: Evan Hahn <69474926+EvanHahn-Signal@users.noreply.github.com>

* nit 2

Co-authored-by: Evan Hahn <69474926+EvanHahn-Signal@users.noreply.github.com>

---------

Co-authored-by: Evan Hahn <69474926+EvanHahn-Signal@users.noreply.github.com>
This commit is contained in:
Harry 2023-03-22 07:52:34 -07:00 committed by GitHub
parent 0c5ab8b09d
commit 68f34d10cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 274 additions and 25 deletions

View File

@ -46,6 +46,12 @@ public protocol RegistrationCoordinator {
/// This gives the user a chance to change it before any automatic steps are taken.
func submitE164(_ e164: E164) -> Guarantee<RegistrationStep>
/// Wipes any previously submitted E164 so the user can enter a new one.
///
/// May also blow away in progress registration steps and require redoing them;
/// a new number is essentially a new registration.
func requestChangeE164() -> Guarantee<RegistrationStep>
/// Request an SMS code be sent, returning the next step to take.
/// If requesting a code is disallowed for any reason, the next step will be the same current
/// step but with attached metadata giving more info on the reason.

View File

@ -102,32 +102,29 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
}
public func submitE164(_ e164: E164) -> Guarantee<RegistrationStep> {
// TODO[Registration] do some validation on the e164 format?
// maybe we trust the view controller to do that.
db.write { tx in
updatePersistedState(tx) {
$0.e164 = e164
}
}
inMemoryState.hasEnteredE164 = true
switch getPathway() {
case .opening:
// Now we transition to the session path since
// we are submitting an e164.
return self.startSession(e164: e164)
case .registrationRecoveryPassword(let password):
return nextStepForRegRecoveryPasswordPath(regRecoveryPw: password)
case .kbsAuthCredential:
owsFailBeta("Shouldn't be submitting an e164 for a known valid kbs auth credential")
return nextStep()
case .kbsAuthCredentialCandidates:
return nextStep()
case .session:
return self.startSession(e164: e164)
case .profileSetup:
owsFailBeta("Shouldn't be submitting an e164 in profile setup")
return nextStep()
return nextStep()
}
public func requestChangeE164() -> Guarantee<RegistrationStep> {
db.write { tx in
updatePersistedState(tx) {
$0.e164 = nil
}
// Reset the session; it is e164 dependent.
resetSession(tx)
// Reload auth credential candidates; we might not have
// had a credential for the old e164 but might have one for
// the new e164!
loadkbsAuthCredentialCandidates(tx)
}
inMemoryState.hasEnteredE164 = false
return nextStep()
}
public func requestSMSCode() -> Guarantee<RegistrationStep> {
@ -606,10 +603,7 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
inMemoryState.isV12faUser = true
}
let kbsAuthCredentialCandidates = deps.kbsAuthCredentialStore.getAuthCredentials(tx)
if kbsAuthCredentialCandidates.isEmpty.negated {
inMemoryState.kbsAuthCredentialCandidates = kbsAuthCredentialCandidates
}
loadkbsAuthCredentialCandidates(tx)
inMemoryState.isManualMessageFetchEnabled = deps.tsAccountManager.isManualMessageFetchEnabled(tx)
inMemoryState.registrationId = deps.tsAccountManager.getOrGenerateRegistrationId(tx)
inMemoryState.pniRegistrationId = deps.tsAccountManager.getOrGeneratePniRegistrationId(tx)
@ -1058,6 +1052,13 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
}
}
private func loadkbsAuthCredentialCandidates(_ tx: DBReadTransaction) {
let kbsAuthCredentialCandidates = deps.kbsAuthCredentialStore.getAuthCredentials(tx)
if kbsAuthCredentialCandidates.isEmpty.negated {
inMemoryState.kbsAuthCredentialCandidates = kbsAuthCredentialCandidates
}
}
private func wipeInMemoryStateToPreventKBSPathAttempts() {
inMemoryState.reglockToken = nil
inMemoryState.regRecoveryPw = nil

View File

@ -279,7 +279,7 @@ extension RegistrationNavigationController: RegistrationCaptchaPresenter {
extension RegistrationNavigationController: RegistrationVerificationPresenter {
func returnToPhoneNumberEntry() {
// TODO[Registration]: figure out our story on going back.
pushNextController(coordinator.requestChangeE164())
}
func requestSMSCode() {

View File

@ -1077,6 +1077,116 @@ public class RegistrationCoordinatorTest: XCTestCase {
}
}
func testKBSAuthCredentialPath_noMatchingCredentialsThenChangeNumber() {
executeTest {
// Don't care about timing, just start it.
scheduler.start()
// Set profile info so we skip those steps.
setupDefaultAccountAttributes()
// Put some auth credentials in storage.
let credentialCandidates: [KBSAuthCredential] = [
Stubs.kbsAuthCredential
]
kbsAuthCredentialStore.dict = Dictionary(grouping: credentialCandidates, by: \.username).mapValues { $0.first! }
// Get past the opening.
goThroughOpeningHappyPath(expectedNextStep: .phoneNumberEntry(Stubs.phoneNumberEntryState(mode: mode)))
scheduler.stop()
scheduler.adjustTime(to: 0)
let originalE164 = E164("+17875550100")!
let changedE164 = E164("+17875550101")!
// Give it a phone number, which should cause it to check the auth credentials.
var nextStep = coordinator.submitE164(originalE164)
// Don't give back any matches at t=2, which means we will want to create a session as a fallback.
var expectedKBSCheckRequest = RegistrationRequestFactory.kbsAuthCredentialCheckRequest(
e164: originalE164,
credentials: credentialCandidates
)
mockURLSession.addResponse(
TSRequestOWSURLSessionMock.Response(
urlSuffix: expectedKBSCheckRequest.url!.absoluteString,
statusCode: 200,
bodyJson: RegistrationServiceResponses.KBSAuthCheckResponse(matches: [
"\(Stubs.kbsAuthCredential.username):\(Stubs.kbsAuthCredential.credential.password)": .notMatch
])
),
atTime: 2,
on: scheduler
)
// Once the first request fails, at t=2, it should try an start a session.
scheduler.run(atTime: 1) {
// We'll ask for a push challenge, though we don't need to resolve it in this test.
self.pushRegistrationManagerMock.receivePreAuthChallengeTokenMock = {
return Guarantee<String>.pending().0
}
// Resolve with a session at time 3.
self.sessionManager.beginSessionResponse = self.scheduler.guarantee(
resolvingWith: .success(Stubs.session(e164: originalE164, hasSentVerificationCode: false)),
atTime: 3
)
}
// Then when it gets back the session at t=3, it should immediately ask for
// a verification code to be sent.
scheduler.run(atTime: 3) {
// Resolve with an updated session at time 4.
self.sessionManager.requestCodeResponse = self.scheduler.guarantee(
resolvingWith: .success(Stubs.session(hasSentVerificationCode: true)),
atTime: 4
)
}
pushRegistrationManagerMock.requestPushTokenMock = { .value(Stubs.apnsToken)}
scheduler.runUntilIdle()
XCTAssertEqual(scheduler.currentTime, 4)
// Now we should expect to be at verification code entry since we already set the phone number.
XCTAssertEqual(nextStep.value, .verificationCodeEntry(Stubs.verificationCodeEntryState()))
// We should have wiped the invalid and unknown credentials.
let remainingCredentials = kbsAuthCredentialStore.dict
XCTAssertNotNil(remainingCredentials[Stubs.kbsAuthCredential.username])
// Now change the phone number; this should take us back to phone number entry.
nextStep = coordinator.requestChangeE164()
scheduler.runUntilIdle()
XCTAssertEqual(nextStep.value, .phoneNumberEntry(Stubs.phoneNumberEntryState(mode: mode)))
// Give it a phone number, which should cause it to check the auth credentials again.
nextStep = coordinator.submitE164(changedE164)
// Give a match at t=5, so it registers via kbs auth credential.
expectedKBSCheckRequest = RegistrationRequestFactory.kbsAuthCredentialCheckRequest(
e164: changedE164,
credentials: credentialCandidates
)
mockURLSession.addResponse(
TSRequestOWSURLSessionMock.Response(
urlSuffix: expectedKBSCheckRequest.url!.absoluteString,
statusCode: 200,
bodyJson: RegistrationServiceResponses.KBSAuthCheckResponse(matches: [
"\(Stubs.kbsAuthCredential.username):\(Stubs.kbsAuthCredential.credential.password)": .match
])
),
atTime: 5,
on: scheduler
)
// Now it should ask for PIN entry; we are on the kbs auth credential path.
scheduler.runUntilIdle()
XCTAssertEqual(nextStep.value, .pinEntry(Stubs.pinEntryStateForKBSAuthCredentialPath()))
}
}
// MARK: - Session Path
public func testSessionPath_happyPath() {
@ -1389,6 +1499,135 @@ public class RegistrationCoordinatorTest: XCTestCase {
}
}
public func testSessionPath_changeE164() {
executeTest {
setUpSessionPath()
let originalE164 = E164("+17875550100")!
let changedE164 = E164("+17875550101")!
// Give it a phone number, which should cause it to start a session.
var nextStep = coordinator.submitE164(originalE164)
// We'll ask for a push challenge, though we won't resolve it in this test.
self.pushRegistrationManagerMock.receivePreAuthChallengeTokenMock = {
return Guarantee<String>.pending().0
}
// At t=2, give back a session that's ready to go.
self.sessionManager.beginSessionResponse = self.scheduler.guarantee(
resolvingWith: .success(RegistrationSession(
id: Stubs.sessionId,
e164: originalE164,
receivedDate: date,
nextSMS: 0,
nextCall: 0,
nextVerificationAttempt: nil,
allowedToRequestCode: true,
requestedInformation: [],
hasUnknownChallengeRequiringAppUpdate: false,
verified: false
)),
atTime: 2
)
// Once we get that session at t=2, we should try and send a code.
// Be ready for that starting at t=1 (but not before).
scheduler.run(atTime: 1) {
// Give back a session with a sent code.
self.sessionManager.requestCodeResponse = self.scheduler.guarantee(
resolvingWith: .success(RegistrationSession(
id: Stubs.sessionId,
e164: originalE164,
receivedDate: self.date,
nextSMS: 0,
nextCall: 0,
nextVerificationAttempt: 0,
allowedToRequestCode: true,
requestedInformation: [],
hasUnknownChallengeRequiringAppUpdate: false,
verified: false
)),
atTime: 3
)
}
// We should be on the verification code entry screen.
scheduler.runUntilIdle()
XCTAssertEqual(scheduler.currentTime, 3)
XCTAssertEqual(
nextStep.value,
.verificationCodeEntry(
Stubs.verificationCodeEntryState(e164: originalE164)
)
)
// Ask to change the number; this should put us back on phone number entry.
nextStep = coordinator.requestChangeE164()
scheduler.runUntilIdle()
XCTAssertEqual(
nextStep.value,
.phoneNumberEntry(Stubs.phoneNumberEntryState(mode: mode))
)
// Give it the new phone number, which should cause it to start a session.
nextStep = coordinator.submitE164(changedE164)
// We'll ask for a push challenge, though we won't resolve it in this test.
self.pushRegistrationManagerMock.receivePreAuthChallengeTokenMock = {
return Guarantee<String>.pending().0
}
// At t=5, give back a session that's ready to go.
self.sessionManager.beginSessionResponse = self.scheduler.guarantee(
resolvingWith: .success(RegistrationSession(
id: Stubs.sessionId,
e164: changedE164,
receivedDate: date,
nextSMS: 0,
nextCall: 0,
nextVerificationAttempt: nil,
allowedToRequestCode: true,
requestedInformation: [],
hasUnknownChallengeRequiringAppUpdate: false,
verified: false
)),
atTime: 5
)
// Once we get that session at t=5, we should try and send a code.
// Be ready for that starting at t=4 (but not before).
scheduler.run(atTime: 4) {
// Give back a session with a sent code.
self.sessionManager.requestCodeResponse = self.scheduler.guarantee(
resolvingWith: .success(RegistrationSession(
id: Stubs.sessionId,
e164: changedE164,
receivedDate: self.date,
nextSMS: 0,
nextCall: 0,
nextVerificationAttempt: 0,
allowedToRequestCode: true,
requestedInformation: [],
hasUnknownChallengeRequiringAppUpdate: false,
verified: false
)),
atTime: 6
)
}
// We should be on the verification code entry screen.
scheduler.runUntilIdle()
XCTAssertEqual(scheduler.currentTime, 6)
XCTAssertEqual(
nextStep.value,
.verificationCodeEntry(
Stubs.verificationCodeEntryState(e164: changedE164)
)
)
}
}
public func testSessionPath_captchaChallenge() {
executeTest {
setUpSessionPath()
@ -2582,7 +2821,10 @@ public class RegistrationCoordinatorTest: XCTestCase {
)
}
static func session(hasSentVerificationCode: Bool) -> RegistrationSession {
static func session(
e164: E164 = Stubs.e164,
hasSentVerificationCode: Bool
) -> RegistrationSession {
return RegistrationSession(
id: UUID().uuidString,
e164: e164,