mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-05 19:28:46 +02:00
Update
This commit is contained in:
parent
4f6ba2c2d0
commit
7171441b96
7 changed files with 199 additions and 213 deletions
|
|
@ -1,4 +1,5 @@
|
||||||
import XCTest
|
import XCTest
|
||||||
|
import Foundation
|
||||||
|
|
||||||
class UITests: XCTestCase {
|
class UITests: XCTestCase {
|
||||||
private var app: XCUIApplication!
|
private var app: XCUIApplication!
|
||||||
|
|
@ -13,12 +14,24 @@ class UITests: XCTestCase {
|
||||||
app = nil
|
app = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Deletes a test account so the sign-up flow can be exercised again.
|
||||||
|
/// Launches the app with `--delete-test-account` which logs in and deletes the account.
|
||||||
|
private func deleteTestAccount(phone: String) {
|
||||||
|
let cleanupApp = XCUIApplication()
|
||||||
|
cleanupApp.launchArguments += ["--ui-test", "--delete-test-account", phone]
|
||||||
|
cleanupApp.launch()
|
||||||
|
let success = cleanupApp.windows["DeleteAccount.Success"]
|
||||||
|
XCTAssert(success.waitForExistence(timeout: 30), "test account cleanup did not complete in 30s")
|
||||||
|
cleanupApp.terminate()
|
||||||
|
}
|
||||||
|
|
||||||
func testLaunch() throws {
|
func testLaunch() throws {
|
||||||
app.launch()
|
app.launch()
|
||||||
XCTAssert(app.wait(for: .runningForeground, timeout: 10.0))
|
XCTAssert(app.wait(for: .runningForeground, timeout: 10.0))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testLoginToSetName() throws {
|
func testLoginToSetName() throws {
|
||||||
|
deleteTestAccount(phone: "9996625678")
|
||||||
app.launch()
|
app.launch()
|
||||||
|
|
||||||
// Welcome screen — tap Start Messaging
|
// Welcome screen — tap Start Messaging
|
||||||
|
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
{
|
|
||||||
"pins" : [
|
|
||||||
{
|
|
||||||
"identity" : "tdlibframework",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/Swiftgram/TDLibFramework",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "fdda50b9335171329237fab2381cbc2e6e3ce86c",
|
|
||||||
"version" : "1.8.60-cb863c16"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "tdlibkit",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/Swiftgram/TDLibKit",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "245888f853f5e304029f4fa423af14a045476f30",
|
|
||||||
"version" : "1.5.2-tdlib-1.8.60-cb863c16"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"version" : 2
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
// swift-tools-version: 5.9
|
|
||||||
import PackageDescription
|
|
||||||
|
|
||||||
let package = Package(
|
|
||||||
name: "test-helper",
|
|
||||||
platforms: [.macOS(.v13)],
|
|
||||||
dependencies: [
|
|
||||||
.package(url: "https://github.com/Swiftgram/TDLibKit", exact: "1.5.2-tdlib-1.8.60-cb863c16"),
|
|
||||||
],
|
|
||||||
targets: [
|
|
||||||
.executableTarget(
|
|
||||||
name: "test-helper",
|
|
||||||
dependencies: ["TDLibKit"]
|
|
||||||
),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
@ -1,165 +0,0 @@
|
||||||
import Foundation
|
|
||||||
import TDLibKit
|
|
||||||
import TDLibFramework
|
|
||||||
|
|
||||||
// MARK: - Argument parsing
|
|
||||||
|
|
||||||
func printUsage() -> Never {
|
|
||||||
fputs("Usage: test-helper delete-account --api-id <id> --api-hash <hash> --phone <number>\n", stderr)
|
|
||||||
exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseArgs() -> (apiId: Int, apiHash: String, phone: String) {
|
|
||||||
let args = CommandLine.arguments
|
|
||||||
guard args.count >= 2, args[1] == "delete-account" else { printUsage() }
|
|
||||||
|
|
||||||
var apiId: Int?
|
|
||||||
var apiHash: String?
|
|
||||||
var phone: String?
|
|
||||||
|
|
||||||
var i = 2
|
|
||||||
while i < args.count {
|
|
||||||
switch args[i] {
|
|
||||||
case "--api-id":
|
|
||||||
i += 1; guard i < args.count, let v = Int(args[i]) else { printUsage() }
|
|
||||||
apiId = v
|
|
||||||
case "--api-hash":
|
|
||||||
i += 1; guard i < args.count else { printUsage() }
|
|
||||||
apiHash = args[i]
|
|
||||||
case "--phone":
|
|
||||||
i += 1; guard i < args.count else { printUsage() }
|
|
||||||
phone = args[i]
|
|
||||||
default:
|
|
||||||
printUsage()
|
|
||||||
}
|
|
||||||
i += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let apiId, let apiHash, let phone else { printUsage() }
|
|
||||||
return (apiId, apiHash, phone)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Phone number validation
|
|
||||||
|
|
||||||
/// Parses a test phone number (format: 99966XYYYY or +99966XYYYY).
|
|
||||||
/// Returns (fullNumber with + prefix, verification code).
|
|
||||||
func parseTestPhone(_ phone: String) -> (fullNumber: String, code: String)? {
|
|
||||||
let digits = phone.hasPrefix("+") ? String(phone.dropFirst()) : phone
|
|
||||||
guard digits.count == 10, digits.hasPrefix("99966") else { return nil }
|
|
||||||
let dcDigit = digits[digits.index(digits.startIndex, offsetBy: 5)]
|
|
||||||
guard dcDigit >= "1", dcDigit <= "3" else { return nil }
|
|
||||||
let code = String(repeating: dcDigit, count: 5)
|
|
||||||
let fullNumber = "+\(digits)"
|
|
||||||
return (fullNumber, code)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - TDLib account deletion
|
|
||||||
|
|
||||||
func deleteTestAccount(apiId: Int, apiHash: String, phone: String, code: String) async throws {
|
|
||||||
let tmpDir = FileManager.default.temporaryDirectory
|
|
||||||
.appendingPathComponent("test-helper-\(ProcessInfo.processInfo.processIdentifier)")
|
|
||||||
try FileManager.default.createDirectory(at: tmpDir, withIntermediateDirectories: true)
|
|
||||||
defer { try? FileManager.default.removeItem(at: tmpDir) }
|
|
||||||
|
|
||||||
// Suppress TDLib's verbose C++ logging
|
|
||||||
td_execute("{\"@type\":\"setLogVerbosityLevel\",\"new_verbosity_level\":0}")
|
|
||||||
|
|
||||||
let manager = TDLibClientManager()
|
|
||||||
defer { manager.closeClients() }
|
|
||||||
|
|
||||||
let authState = AsyncStream.makeStream(of: AuthorizationState.self)
|
|
||||||
|
|
||||||
let client = manager.createClient { data, client in
|
|
||||||
guard let update = try? client.decoder.decode(Update.self, from: data) else { return }
|
|
||||||
if case .updateAuthorizationState(let s) = update {
|
|
||||||
authState.continuation.yield(s.authorizationState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for await state in authState.stream {
|
|
||||||
switch state {
|
|
||||||
case .authorizationStateWaitTdlibParameters:
|
|
||||||
try await client.setTdlibParameters(
|
|
||||||
apiHash: apiHash,
|
|
||||||
apiId: apiId,
|
|
||||||
applicationVersion: "1.0",
|
|
||||||
databaseDirectory: tmpDir.path,
|
|
||||||
databaseEncryptionKey: Data(),
|
|
||||||
deviceModel: "test-helper",
|
|
||||||
filesDirectory: tmpDir.appendingPathComponent("files").path,
|
|
||||||
systemLanguageCode: "en",
|
|
||||||
systemVersion: "macOS",
|
|
||||||
useChatInfoDatabase: false,
|
|
||||||
useFileDatabase: false,
|
|
||||||
useMessageDatabase: false,
|
|
||||||
useSecretChats: false,
|
|
||||||
useTestDc: true
|
|
||||||
)
|
|
||||||
|
|
||||||
case .authorizationStateWaitPhoneNumber:
|
|
||||||
try await client.setAuthenticationPhoneNumber(
|
|
||||||
phoneNumber: phone,
|
|
||||||
settings: PhoneNumberAuthenticationSettings?.none
|
|
||||||
)
|
|
||||||
|
|
||||||
case .authorizationStateWaitCode:
|
|
||||||
try await client.checkAuthenticationCode(code: code)
|
|
||||||
|
|
||||||
case .authorizationStateWaitRegistration:
|
|
||||||
print("No account for \(phone) (sign-up requested). Nothing to delete.")
|
|
||||||
authState.continuation.finish()
|
|
||||||
return
|
|
||||||
|
|
||||||
case .authorizationStateReady:
|
|
||||||
print("Logged in. Deleting account...")
|
|
||||||
try await client.deleteAccount(password: String?.none, reason: "test cleanup")
|
|
||||||
print("Account deleted.")
|
|
||||||
authState.continuation.finish()
|
|
||||||
return
|
|
||||||
|
|
||||||
case .authorizationStateClosed:
|
|
||||||
authState.continuation.finish()
|
|
||||||
return
|
|
||||||
|
|
||||||
default:
|
|
||||||
fputs("Unexpected auth state: \(state)\n", stderr)
|
|
||||||
authState.continuation.finish()
|
|
||||||
throw NSError(domain: "test-helper", code: 1, userInfo: [
|
|
||||||
NSLocalizedDescriptionKey: "Unexpected auth state"
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Main
|
|
||||||
|
|
||||||
let (apiId, apiHash, phone) = parseArgs()
|
|
||||||
|
|
||||||
guard let parsed = parseTestPhone(phone) else {
|
|
||||||
fputs("Error: phone must match format 99966XYYYY (X = 1, 2, or 3)\n", stderr)
|
|
||||||
exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
|
||||||
try await withThrowingTaskGroup(of: Void.self) { group in
|
|
||||||
group.addTask {
|
|
||||||
try await deleteTestAccount(apiId: apiId, apiHash: apiHash, phone: parsed.fullNumber, code: parsed.code)
|
|
||||||
}
|
|
||||||
group.addTask {
|
|
||||||
try await Task.sleep(for: .seconds(30))
|
|
||||||
throw NSError(domain: "test-helper", code: 1, userInfo: [
|
|
||||||
NSLocalizedDescriptionKey: "Timed out after 30 seconds"
|
|
||||||
])
|
|
||||||
}
|
|
||||||
try await group.next()
|
|
||||||
group.cancelAll()
|
|
||||||
}
|
|
||||||
} catch let error as TDLibKit.Error {
|
|
||||||
fputs("Error: [\(error.code)] \(error.message)\n", stderr)
|
|
||||||
exit(1)
|
|
||||||
} catch {
|
|
||||||
fputs("Error: \(error)\n", stderr)
|
|
||||||
exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
exit(0)
|
|
||||||
|
|
@ -123,17 +123,23 @@ Test numbers are still subject to flood limits. If a number gets rate-limited, p
|
||||||
|
|
||||||
### Deleting Test Accounts
|
### Deleting Test Accounts
|
||||||
|
|
||||||
Tests that exercise the sign-up flow create an account on the test servers. Re-running the same test with the same phone number will skip sign-up because the account already exists. To get a fresh sign-up screen, delete the account first with the `test-helper` CLI:
|
Tests that exercise the sign-up flow create an account on the test servers. Re-running the same test with the same phone number will skip sign-up because the account already exists. To get a fresh sign-up screen, delete the account before running the test.
|
||||||
|
|
||||||
```bash
|
The app supports a `--delete-test-account <phone>` launch argument. When combined with `--ui-test`, the app logs into the test account, deletes it, and exits — no UI is shown. The verification code is derived automatically from the phone number (DC digit repeated 5 times).
|
||||||
cd build-system/test-helper
|
|
||||||
swift run test-helper delete-account \
|
In UI tests, use this by launching a separate app instance:
|
||||||
--api-id <id> --api-hash <hash> --phone 9996625678
|
|
||||||
|
```swift
|
||||||
|
private func deleteTestAccount(phone: String) {
|
||||||
|
let cleanupApp = XCUIApplication()
|
||||||
|
cleanupApp.launchArguments += ["--ui-test", "--delete-test-account", phone]
|
||||||
|
cleanupApp.launch()
|
||||||
|
let terminated = cleanupApp.wait(for: .notRunning, timeout: 30)
|
||||||
|
XCTAssert(terminated, "test account cleanup did not complete in 30s")
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The tool connects to the test datacenters, authenticates with the given number, and deletes the account. If no account exists, it exits successfully. Run it before any test that needs a clean sign-up flow.
|
Call `deleteTestAccount(phone:)` at the start of any test that needs a clean sign-up flow. The phone number format is `99966XYYYY` (no `+` prefix needed).
|
||||||
|
|
||||||
The `--api-id` and `--api-hash` are your Telegram API credentials from [my.telegram.org](https://my.telegram.org).
|
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1623,3 +1623,146 @@ func _internal_reportMissingCode(network: Network, phoneNumber: String, phoneCod
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum TestLoginAndDeleteAccountError {
|
||||||
|
case generic
|
||||||
|
}
|
||||||
|
|
||||||
|
public func test_loginAndDeleteAccount(
|
||||||
|
rootPath: String,
|
||||||
|
accountManager: AccountManager<TelegramAccountManagerTypes>,
|
||||||
|
networkArguments: NetworkInitializationArguments,
|
||||||
|
encryptionParameters: ValueBoxEncryptionParameters,
|
||||||
|
phoneNumber: String,
|
||||||
|
phoneCode: String
|
||||||
|
) -> Signal<Never, TestLoginAndDeleteAccountError> {
|
||||||
|
Logger.shared.logToConsole = true
|
||||||
|
|
||||||
|
return accountManager.transaction{ transaction -> AccountRecordId? in
|
||||||
|
let record = transaction.createAuth([.environment(AccountEnvironmentAttribute(environment: .test))])
|
||||||
|
return record?.id
|
||||||
|
}
|
||||||
|
|> castError(TestLoginAndDeleteAccountError.self)
|
||||||
|
|> mapToSignal { accountId -> Signal<UnauthorizedAccount, TestLoginAndDeleteAccountError> in
|
||||||
|
guard let accountId else {
|
||||||
|
preconditionFailure("Account not found")
|
||||||
|
}
|
||||||
|
return accountWithId(
|
||||||
|
accountManager: accountManager,
|
||||||
|
networkArguments: networkArguments,
|
||||||
|
id: accountId,
|
||||||
|
encryptionParameters: encryptionParameters,
|
||||||
|
supplementary: true,
|
||||||
|
isSupportUser: false,
|
||||||
|
rootPath: rootPath,
|
||||||
|
beginWithTestingEnvironment: true,
|
||||||
|
backupData: nil,
|
||||||
|
auxiliaryMethods: AccountAuxiliaryMethods(fetchResource: { _, _, _, _ in
|
||||||
|
return nil
|
||||||
|
}, fetchResourceMediaReferenceHash: { resource in
|
||||||
|
return .single(nil)
|
||||||
|
}, prepareSecretThumbnailData: { data in
|
||||||
|
return nil
|
||||||
|
}, backgroundUpload: { postbox, _, resource in
|
||||||
|
return .single(nil)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|> castError(TestLoginAndDeleteAccountError.self)
|
||||||
|
|> mapToSignal { account -> Signal<UnauthorizedAccount, TestLoginAndDeleteAccountError> in
|
||||||
|
switch account {
|
||||||
|
case .upgrading:
|
||||||
|
preconditionFailure("Unexpected account state: upgrading")
|
||||||
|
case let .unauthorized(account):
|
||||||
|
return .single(account)
|
||||||
|
case .authorized:
|
||||||
|
preconditionFailure("Unexpected account state: authorized")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|> mapToSignal { account -> Signal<(UnauthorizedAccount, UnauthorizedAccountStateContents), TestLoginAndDeleteAccountError> in
|
||||||
|
account.network.shouldKeepConnection.set(.single(true))
|
||||||
|
|
||||||
|
return sendAuthorizationCode(
|
||||||
|
accountManager: accountManager,
|
||||||
|
account: account,
|
||||||
|
phoneNumber: phoneNumber,
|
||||||
|
apiId: networkArguments.apiId,
|
||||||
|
apiHash: networkArguments.apiHash,
|
||||||
|
pushNotificationConfiguration: nil,
|
||||||
|
firebaseSecretStream: .never(),
|
||||||
|
syncContacts: false,
|
||||||
|
forcedPasswordSetupNotice: { _ in nil }
|
||||||
|
)
|
||||||
|
|> mapError { _ -> TestLoginAndDeleteAccountError in
|
||||||
|
return .generic
|
||||||
|
}
|
||||||
|
|> mapToSignal { result -> Signal<(UnauthorizedAccount, UnauthorizedAccountStateContents), TestLoginAndDeleteAccountError> in
|
||||||
|
switch result {
|
||||||
|
case .loggedIn:
|
||||||
|
preconditionFailure("Unexpected send code state: logged in")
|
||||||
|
case let .sentCode(account):
|
||||||
|
return account.postbox.transaction { transaction -> UnauthorizedAccountStateContents? in
|
||||||
|
guard let state = transaction.getState() as? UnauthorizedAccountState else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return state.contents
|
||||||
|
}
|
||||||
|
|> castError(TestLoginAndDeleteAccountError.self)
|
||||||
|
|> mapToSignal { state -> Signal<(UnauthorizedAccount, UnauthorizedAccountStateContents), TestLoginAndDeleteAccountError> in
|
||||||
|
guard let state else {
|
||||||
|
preconditionFailure("Unexpected account state: nil")
|
||||||
|
}
|
||||||
|
return .single((account, state))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|> mapToSignal { account, state -> Signal<(UnauthorizedAccount, AuthorizeWithCodeResult), TestLoginAndDeleteAccountError> in
|
||||||
|
account.network.shouldKeepConnection.set(.single(true))
|
||||||
|
|
||||||
|
switch state {
|
||||||
|
case let .confirmationCodeEntry(_, type, _, _, _, _, _, _):
|
||||||
|
switch type {
|
||||||
|
case let .call(length), let .sms(length), let .otherSession(length):
|
||||||
|
if phoneCode.count != length {
|
||||||
|
preconditionFailure("Unexpected sent code length: \(length) != \(phoneCode.count)")
|
||||||
|
}
|
||||||
|
|
||||||
|
return authorizeWithCode(
|
||||||
|
accountManager: accountManager,
|
||||||
|
account: account,
|
||||||
|
code: .phoneCode(phoneCode),
|
||||||
|
termsOfService: nil,
|
||||||
|
forcedPasswordSetupNotice: { _ in nil }
|
||||||
|
)
|
||||||
|
|> mapError { _ -> TestLoginAndDeleteAccountError in
|
||||||
|
return .generic
|
||||||
|
}
|
||||||
|
|> mapToSignal { result -> Signal<(UnauthorizedAccount, AuthorizeWithCodeResult), TestLoginAndDeleteAccountError> in
|
||||||
|
return .single((account, result))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
preconditionFailure("Unexpected sent code type: \(type)")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
preconditionFailure("Unexpected account state: \(state)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|> mapToSignal { account, checkCodeResult -> Signal<Never, TestLoginAndDeleteAccountError> in
|
||||||
|
switch checkCodeResult {
|
||||||
|
case .signUp:
|
||||||
|
return .complete()
|
||||||
|
case .loggedIn:
|
||||||
|
return account.network.request(Api.functions.account.deleteAccount(
|
||||||
|
flags: 0,
|
||||||
|
reason: "",
|
||||||
|
password: nil
|
||||||
|
))
|
||||||
|
|> mapError { _ -> TestLoginAndDeleteAccountError in
|
||||||
|
return .generic
|
||||||
|
}
|
||||||
|
|> mapToSignal { _ -> Signal<Never, TestLoginAndDeleteAccountError> in
|
||||||
|
return .complete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -999,7 +999,35 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
|
||||||
|
|
||||||
telegramUIDeclareEncodables()
|
telegramUIDeclareEncodables()
|
||||||
initializeAccountManagement()
|
initializeAccountManagement()
|
||||||
|
|
||||||
|
if isUITest,
|
||||||
|
let deleteIdx = CommandLine.arguments.firstIndex(of: "--delete-test-account"),
|
||||||
|
deleteIdx + 1 < CommandLine.arguments.count
|
||||||
|
{
|
||||||
|
let phone = CommandLine.arguments[deleteIdx + 1]
|
||||||
|
let digits = phone.hasPrefix("+") ? String(phone.dropFirst()) : phone
|
||||||
|
guard digits.count == 10, digits.hasPrefix("99966") else {
|
||||||
|
preconditionFailure("--delete-test-account phone must match 99966XYYYY")
|
||||||
|
}
|
||||||
|
let dcDigit = digits[digits.index(digits.startIndex, offsetBy: 5)]
|
||||||
|
let phoneCode = String(repeating: dcDigit, count: 5)
|
||||||
|
|
||||||
|
let _ = test_loginAndDeleteAccount(
|
||||||
|
rootPath: rootPath,
|
||||||
|
accountManager: accountManager,
|
||||||
|
networkArguments: networkArguments,
|
||||||
|
encryptionParameters: encryptionParameters,
|
||||||
|
phoneNumber: "+\(digits)",
|
||||||
|
phoneCode: phoneCode
|
||||||
|
).start(error: { _ in
|
||||||
|
preconditionFailure("test_loginAndDeleteAccount failed")
|
||||||
|
}, completed: { [weak self] in
|
||||||
|
self?.window?.accessibilityIdentifier = "DeleteAccount.Success"
|
||||||
|
})
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
let pushRegistry = PKPushRegistry(queue: .main)
|
let pushRegistry = PKPushRegistry(queue: .main)
|
||||||
if #available(iOS 9.0, *) {
|
if #available(iOS 9.0, *) {
|
||||||
pushRegistry.desiredPushTypes = Set([.voIP])
|
pushRegistry.desiredPushTypes = Set([.voIP])
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue