TM-SGNL-iOS/SignalServiceKit/tests/Registration/RegistrationSessionManagerTest.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

603 lines
23 KiB
Swift

//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
@testable import SignalServiceKit
public import XCTest
public class RegistrationSessionManagerTest: XCTestCase {
private var registrationSessionManager: RegistrationSessionManagerImpl!
private var date = Date()
private var db: InMemoryDB!
private var kvStore: KeyValueStore!
private var mockURLSession: TSRequestOWSURLSessionMock!
public override func setUp() {
db = InMemoryDB()
let mockURLSession = TSRequestOWSURLSessionMock()
self.mockURLSession = mockURLSession
let mockSignalService = OWSSignalServiceMock()
mockSignalService.mockUrlSessionBuilder = { _, _, _ in
return mockURLSession
}
kvStore = KeyValueStore(
collection: RegistrationSessionManagerImpl.KvStore.collectionName
)
registrationSessionManager = RegistrationSessionManagerImpl(
dateProvider: { self.date },
db: db,
schedulers: DispatchQueueSchedulers(),
signalService: mockSignalService
)
}
public func testParseRegistrationSession() async throws {
let code = "1234"
let oldSession = stubSession()
let expectedRequest = RegistrationRequestFactory.submitVerificationCodeRequest(
sessionId: oldSession.id,
code: code
)
// A standard response
var responseJSON = """
{
"id": "abcd",
"nextSms": 1,
"nextCall": 2,
"nextVerificationAttempt": 3,
"allowedToRequestCode": true,
"requestedInformation": ["captcha", "pushChallenge"],
"verified": false,
}
"""
mockURLSession.addResponse(TSRequestOWSURLSessionMock.Response(
matcher: { $0.url == expectedRequest.url },
statusCode: RegistrationServiceResponses.SubmitVerificationCodeResponseCodes.success.rawValue,
bodyData: responseJSON.data(using: .utf8)
))
do {
let result = await registrationSessionManager.requestVerificationCode(
for: oldSession,
transport: [Registration.CodeTransport.sms, .voice].randomElement()!
).awaitable()
XCTAssertEqual(result, .success(RegistrationSession(
id: "abcd",
e164: oldSession.e164,
receivedDate: self.date,
nextSMS: 1,
nextCall: 2,
nextVerificationAttempt: 3,
allowedToRequestCode: true,
requestedInformation: [.captcha, .pushChallenge],
hasUnknownChallengeRequiringAppUpdate: false,
verified: false
)))
}
// Try empty time durations
responseJSON = """
{
"id": "abcd",
"allowedToRequestCode": true,
"requestedInformation": ["captcha", "pushChallenge"],
"verified": false,
}
"""
mockURLSession.addResponse(TSRequestOWSURLSessionMock.Response(
matcher: { $0.url == expectedRequest.url },
statusCode: RegistrationServiceResponses.SubmitVerificationCodeResponseCodes.success.rawValue,
bodyData: responseJSON.data(using: .utf8)
))
do {
let result = await registrationSessionManager.requestVerificationCode(
for: oldSession,
transport: [Registration.CodeTransport.sms, .voice].randomElement()!
).awaitable()
XCTAssertEqual(result, .success(RegistrationSession(
id: "abcd",
e164: oldSession.e164,
receivedDate: self.date,
nextSMS: nil,
nextCall: nil,
nextVerificationAttempt: nil,
allowedToRequestCode: true,
requestedInformation: [.captcha, .pushChallenge],
hasUnknownChallengeRequiringAppUpdate: false,
verified: false
)))
}
// Try requested info we don't know how to parse.
responseJSON = """
{
"id": "abcd",
"nextSms": 1,
"nextCall": 2,
"nextVerificationAttempt": 3,
"allowedToRequestCode": true,
"requestedInformation": ["someRandomGarbage", "captcha", "pushChallenge"],
"verified": false,
}
"""
mockURLSession.addResponse(TSRequestOWSURLSessionMock.Response(
matcher: { $0.url == expectedRequest.url },
statusCode: RegistrationServiceResponses.SubmitVerificationCodeResponseCodes.success.rawValue,
bodyData: responseJSON.data(using: .utf8)
))
do {
let result = await registrationSessionManager.requestVerificationCode(
for: oldSession,
transport: [Registration.CodeTransport.sms, .voice].randomElement()!
).awaitable()
XCTAssertEqual(result, .success(RegistrationSession(
id: "abcd",
e164: oldSession.e164,
receivedDate: self.date,
nextSMS: 1,
nextCall: 2,
nextVerificationAttempt: 3,
allowedToRequestCode: true,
requestedInformation: [.captcha, .pushChallenge],
// We saw unknown challenges
hasUnknownChallengeRequiringAppUpdate: true,
verified: false
)))
}
}
public func testBeginOrRestoreSession() async throws {
let e164 = E164("+17875550100")!
let apnsToken = "1234"
let beginSessionRequest = RegistrationRequestFactory.beginSessionRequest(
e164: e164,
pushToken: apnsToken,
mcc: nil,
mnc: nil
)
// Without any setup, we should try and begin a new session.
var responseBody = stubWireSession()
var responseSession = sessionConverter(responseBody)
mockURLSession.addResponse(
forUrlSuffix: beginSessionRequest.url!.relativeString,
bodyJson: responseBody
)
do {
let result = await registrationSessionManager.beginOrRestoreSession(
e164: e164,
apnsToken: apnsToken
).awaitable()
XCTAssertEqual(result, .success(responseSession))
}
// The session should be in storage.
var savedSession = try db.read { transaction in
let session: RegistrationSession? = try kvStore.getCodableValue(
forKey: RegistrationSessionManagerImpl.KvStore.sessionKey,
transaction: transaction
)
return session
}
XCTAssertEqual(savedSession, responseSession)
// Now we should get back the same session if we try again, with a request
// only to check its validity.
var fetchSessionRequest = RegistrationRequestFactory.fetchSessionRequest(sessionId: responseSession.id)
// Make a new instance, which shuffles the id
responseBody = stubWireSession()
responseSession = sessionConverter(responseBody)
mockURLSession.addResponse(
forUrlSuffix: fetchSessionRequest.url!.relativeString,
bodyJson: responseBody
)
do {
let result = await registrationSessionManager.beginOrRestoreSession(
e164: e164,
apnsToken: apnsToken
).awaitable()
XCTAssertEqual(result, .success(responseSession))
}
// The new (shuffled id) session should be in storage.
savedSession = try db.read { transaction in
let session: RegistrationSession? = try kvStore.getCodableValue(
forKey: RegistrationSessionManagerImpl.KvStore.sessionKey,
transaction: transaction
)
return session
}
XCTAssertEqual(savedSession, responseSession)
// If we have the service respond that the session is invalid, we should get a fresh
// session.
fetchSessionRequest = RegistrationRequestFactory.fetchSessionRequest(sessionId: responseSession.id)
// Make a new instance, which shuffles the id
responseBody = stubWireSession()
responseSession = sessionConverter(responseBody)
mockURLSession.addResponse(
forUrlSuffix: fetchSessionRequest.url!.relativeString,
statusCode: RegistrationServiceResponses.FetchSessionResponseCodes.missingSession.rawValue
)
mockURLSession.addResponse(
forUrlSuffix: beginSessionRequest.url!.relativeString,
bodyJson: responseBody
)
do {
let result = await registrationSessionManager.beginOrRestoreSession(
e164: e164,
apnsToken: apnsToken
).awaitable()
XCTAssertEqual(result, .success(responseSession))
}
// The new (shuffled id) session should be in storage.
savedSession = try db.read { transaction in
let session: RegistrationSession? = try kvStore.getCodableValue(
forKey: RegistrationSessionManagerImpl.KvStore.sessionKey,
transaction: transaction
)
return session
}
XCTAssertEqual(savedSession, responseSession)
// If we complete the session, that should reset everything and behave like the first time.
db.write { registrationSessionManager.clearPersistedSession($0) }
// Should have no saved session
savedSession = try db.read { transaction in
let session: RegistrationSession? = try kvStore.getCodableValue(
forKey: RegistrationSessionManagerImpl.KvStore.sessionKey,
transaction: transaction
)
return session
}
XCTAssertNil(savedSession)
responseBody = stubWireSession()
responseSession = sessionConverter(responseBody)
mockURLSession.addResponse(
forUrlSuffix: beginSessionRequest.url!.relativeString,
bodyJson: responseBody
)
do {
let result = await registrationSessionManager.beginOrRestoreSession(
e164: e164,
apnsToken: apnsToken
).awaitable()
XCTAssertEqual(result, .success(responseSession))
}
// The session should be in storage.
savedSession = try db.read { transaction in
let session: RegistrationSession? = try kvStore.getCodableValue(
forKey: RegistrationSessionManagerImpl.KvStore.sessionKey,
transaction: transaction
)
return session
}
XCTAssertEqual(savedSession, responseSession)
// If we try and request with a different e164 it should also get a new session and wipe the old one.
let newE164 = E164("+17875550101")!
responseBody = stubWireSession()
responseSession = sessionConverter(responseBody, e164: newE164)
mockURLSession.addResponse(
forUrlSuffix: beginSessionRequest.url!.relativeString,
bodyJson: responseBody
)
do {
let result = await registrationSessionManager.beginOrRestoreSession(
e164: newE164,
apnsToken: apnsToken
).awaitable()
XCTAssertEqual(result, .success(responseSession))
}
// The session should be in storage.
savedSession = try db.read { transaction in
let session: RegistrationSession? = try kvStore.getCodableValue(
forKey: RegistrationSessionManagerImpl.KvStore.sessionKey,
transaction: transaction
)
return session
}
XCTAssertEqual(savedSession, responseSession)
}
public func testFulfillChallenge() async {
let captchaToken = "1234"
let pushChallengeToken = "ABCD"
let oldSession = stubSession()
let expectedRequest = RegistrationRequestFactory.fulfillChallengeRequest(
sessionId: oldSession.id,
captchaToken: captchaToken, // Put both, doesn't matter cuz we just match the url.
pushChallengeToken: pushChallengeToken
)
// will have a new id, which is fine cuz it lets us differentiate.
let responseBody = stubWireSession()
let responseSession = sessionConverter(responseBody)
let statusCodeResponsePairs: [(
RegistrationServiceResponses.FulfillChallengeResponseCodes,
Registration.UpdateSessionResponse,
Bool // expects session in response
)] = [
(.success, .success(responseSession), true),
(.notAccepted, .rejectedArgument(responseSession), true),
(.missingSession, .invalidSession, false),
(.notAccepted, .rejectedArgument(responseSession), true),
(.unexpectedError, .genericError, false)
]
for (statusCode, expectedResponse, sessionInBody) in statusCodeResponsePairs {
mockURLSession.addResponse(
forUrlSuffix: expectedRequest.url!.relativeString,
statusCode: statusCode.rawValue,
bodyJson: sessionInBody ? responseBody : nil
)
let result = await registrationSessionManager.fulfillChallenge(
for: oldSession,
fulfillment: [
Registration.ChallengeFulfillment.captcha(captchaToken),
.pushChallenge(pushChallengeToken)
].randomElement()!
).awaitable()
XCTAssertEqual(result, expectedResponse)
}
}
public func testFulfillChallenge_sucessResponseWithoutBody() async {
let captchaToken = "1234"
let oldSession = stubSession()
let expectedRequest = RegistrationRequestFactory.fulfillChallengeRequest(
sessionId: oldSession.id,
captchaToken: captchaToken,
pushChallengeToken: nil
)
mockURLSession.addResponse(
forUrlSuffix: expectedRequest.url!.relativeString,
statusCode: RegistrationServiceResponses.FulfillChallengeResponseCodes.success.rawValue,
bodyJson: nil /* empty json */
)
do {
let result = await registrationSessionManager.fulfillChallenge(
for: oldSession,
fulfillment: .captcha(captchaToken)
).awaitable()
XCTAssertEqual(result, Registration.UpdateSessionResponse.genericError)
}
}
public func testRequestVerificationCode() async {
let oldSession = stubSession()
let expectedRequest = RegistrationRequestFactory.requestVerificationCodeRequest(
sessionId: oldSession.id,
languageCode: nil,
countryCode: nil,
transport: .sms
)
// will have a new id, which is fine cuz it lets us differentiate.
let responseBody = stubWireSession()
let statusCodeResponsePairs: [(
RegistrationServiceResponses.RequestVerificationCodeResponseCodes,
Registration.UpdateSessionResponse,
Bool // expects session in response
)] = [
(.success, .success(sessionConverter(responseBody)), true),
(.malformedRequest, .genericError, false),
(.disallowed, .disallowed(sessionConverter(responseBody)), true),
(.missingSession, .invalidSession, false),
(.retry, .retryAfterTimeout(sessionConverter(responseBody)), true),
(.unexpectedError, .genericError, false)
]
for (statusCode, expectedResponse, sessionInBody) in statusCodeResponsePairs {
mockURLSession.addResponse(
forUrlSuffix: expectedRequest.url!.relativeString,
statusCode: statusCode.rawValue,
bodyJson: sessionInBody ? responseBody : nil
)
let result = await registrationSessionManager.requestVerificationCode(
for: oldSession,
transport: [Registration.CodeTransport.sms, .voice].randomElement()!
).awaitable()
XCTAssertEqual(result, expectedResponse)
}
}
public func testRequestVerificationCodeServiceError() async {
let oldSession = stubSession()
let expectedRequest = RegistrationRequestFactory.requestVerificationCodeRequest(
sessionId: oldSession.id,
languageCode: nil,
countryCode: nil,
transport: .sms
)
var errorResponseJSON = """
{
"permanentFailure": false,
"reason": "providerRejected"
}
"""
mockURLSession.addResponse(TSRequestOWSURLSessionMock.Response(
matcher: { $0.url == expectedRequest.url },
statusCode: RegistrationServiceResponses.RequestVerificationCodeResponseCodes.providerFailure.rawValue,
bodyData: errorResponseJSON.data(using: .utf8)
))
do {
let result = await registrationSessionManager.requestVerificationCode(
for: oldSession,
transport: [Registration.CodeTransport.sms, .voice].randomElement()!
).awaitable()
XCTAssertEqual(result, .serverFailure(Registration.ServerFailureResponse(
session: oldSession,
isPermanent: false,
reason: .providerRejected
)))
}
errorResponseJSON = """
{
"permanentFailure": false,
"reason": "someRandomValueTheClientCantParse"
}
"""
mockURLSession.addResponse(TSRequestOWSURLSessionMock.Response(
matcher: { $0.url == expectedRequest.url },
statusCode: RegistrationServiceResponses.RequestVerificationCodeResponseCodes.providerFailure.rawValue,
bodyData: errorResponseJSON.data(using: .utf8)
))
do {
let result = await registrationSessionManager.requestVerificationCode(
for: oldSession,
transport: [Registration.CodeTransport.sms, .voice].randomElement()!
).awaitable()
XCTAssertEqual(result, .serverFailure(Registration.ServerFailureResponse(
session: oldSession,
isPermanent: false,
reason: nil
)))
}
}
public func testSubmitVerificationCode() async {
let code = "1234"
let oldSession = stubSession()
let expectedRequest = RegistrationRequestFactory.submitVerificationCodeRequest(
sessionId: oldSession.id,
code: code
)
// will have a new ids, which is fine cuz it lets us differentiate.
let verifiedResponseBody = stubWireSession(verified: true)
let verifiedResponseSession = sessionConverter(verifiedResponseBody)
let unVerifiedResponseBody = stubWireSession(verified: false)
let unVerifiedResponseSession = sessionConverter(unVerifiedResponseBody)
let unVerifiedWithNoAttemptResponseBody = stubWireSession(verified: false, hasNextVerificationAttempt: false)
let unVerifiedWithNoAttemptResponseSession = sessionConverter(unVerifiedWithNoAttemptResponseBody)
let statusCodeResponsePairs: [(
RegistrationServiceResponses.SubmitVerificationCodeResponseCodes,
Registration.UpdateSessionResponse,
RegistrationServiceResponses.RegistrationSession?
)] = [
(.success, .success(verifiedResponseSession), verifiedResponseBody),
(.success, .rejectedArgument(unVerifiedResponseSession), unVerifiedResponseBody),
(.malformedRequest, .genericError, nil),
(.missingSession, .invalidSession, nil),
(.newCodeRequired, .success(verifiedResponseSession), verifiedResponseBody),
(.newCodeRequired, .retryAfterTimeout(unVerifiedResponseSession), unVerifiedResponseBody),
(.newCodeRequired, .disallowed(unVerifiedWithNoAttemptResponseSession), unVerifiedWithNoAttemptResponseBody),
(.retry, .retryAfterTimeout(unVerifiedResponseSession), unVerifiedResponseBody),
(.unexpectedError, .genericError, nil)
]
for (statusCode, expectedResponse, sessionInBody) in statusCodeResponsePairs {
mockURLSession.addResponse(
forUrlSuffix: expectedRequest.url!.relativeString,
statusCode: statusCode.rawValue,
bodyJson: sessionInBody
)
let result = await registrationSessionManager.submitVerificationCode(
for: oldSession,
code: code
).awaitable()
XCTAssertEqual(result, expectedResponse)
}
}
// MARK: - Helpers
// MARK: Stub objects
private func stubSession() -> RegistrationSession {
return RegistrationSession(
id: UUID().uuidString,
e164: E164("+17875550100")!, // For our purposes, can be fixed.
receivedDate: date,
nextSMS: 1,
nextCall: 1,
nextVerificationAttempt: nil,
allowedToRequestCode: true,
requestedInformation: [],
hasUnknownChallengeRequiringAppUpdate: false,
verified: false
)
}
private func stubWireSession(
verified: Bool = false,
hasNextVerificationAttempt: Bool = true
) -> RegistrationServiceResponses.RegistrationSession {
return RegistrationServiceResponses.RegistrationSession(
id: UUID().uuidString,
nextSms: (0...100).randomElement(),
nextCall: (0...100).randomElement(),
nextVerificationAttempt: hasNextVerificationAttempt ? (0...100).randomElement() : nil,
allowedToRequestCode: false,
requestedInformation: [.captcha, .pushChallenge],
verified: verified
)
}
// Keep this independent of the production code converter for an extra layer of durability.
private func sessionConverter(
_ wireSession: RegistrationServiceResponses.RegistrationSession,
e164: E164 = E164("+17875550100")!
) -> RegistrationSession {
let requestedInformation: [RegistrationSession.Challenge] = wireSession.requestedInformation.compactMap {
switch $0 {
case .captcha: return .captcha
case .pushChallenge: return .pushChallenge
case .unknown:
XCTFail("If you want to test wire conversion for real be explicit")
return .captcha
}
}
return RegistrationSession(
id: wireSession.id,
e164: e164,
receivedDate: date,
nextSMS: wireSession.nextSms.map { TimeInterval($0) },
nextCall: wireSession.nextCall.map { TimeInterval($0) },
nextVerificationAttempt: wireSession.nextVerificationAttempt.map { TimeInterval($0) },
allowedToRequestCode: wireSession.allowedToRequestCode,
requestedInformation: requestedInformation,
hasUnknownChallengeRequiringAppUpdate: false,
verified: wireSession.verified
)
}
}