fix: use v9 store status for deregistration login
This commit is contained in:
@@ -57,6 +57,7 @@ enum LoginValidationError: Equatable {
|
|||||||
enum LoginResolution {
|
enum LoginResolution {
|
||||||
case completed(V9AuthResponse, AccountSwitchAccount)
|
case completed(V9AuthResponse, AccountSwitchAccount)
|
||||||
case needsAccountSelection(AccountSelectionPayload)
|
case needsAccountSelection(AccountSelectionPayload)
|
||||||
|
case needsDeregistrationConfirmation(DeregistrationLoginConfirmation)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 登录流程错误实体,表示 token、账号列表或账号 ID 异常。
|
/// 登录流程错误实体,表示 token、账号列表或账号 ID 异常。
|
||||||
@@ -124,8 +125,40 @@ struct AccountSwitchAccount: Identifiable, Hashable {
|
|||||||
let storeId: Int?
|
let storeId: Int?
|
||||||
let storeName: String
|
let storeName: String
|
||||||
let scenicId: Int?
|
let scenicId: Int?
|
||||||
|
let status: Int
|
||||||
let isCurrent: Bool
|
let isCurrent: Bool
|
||||||
|
|
||||||
|
/// 创建统一账号展示模型;门店状态默认为正常,兼容尚未返回 `status` 的旧响应。
|
||||||
|
init(
|
||||||
|
accountType: String,
|
||||||
|
businessUserId: Int,
|
||||||
|
title: String,
|
||||||
|
subtitle: String,
|
||||||
|
phone: String,
|
||||||
|
realName: String,
|
||||||
|
avatar: String,
|
||||||
|
scenicName: String,
|
||||||
|
storeId: Int?,
|
||||||
|
storeName: String,
|
||||||
|
scenicId: Int?,
|
||||||
|
status: Int = 1,
|
||||||
|
isCurrent: Bool
|
||||||
|
) {
|
||||||
|
self.accountType = accountType
|
||||||
|
self.businessUserId = businessUserId
|
||||||
|
self.title = title
|
||||||
|
self.subtitle = subtitle
|
||||||
|
self.phone = phone
|
||||||
|
self.realName = realName
|
||||||
|
self.avatar = avatar
|
||||||
|
self.scenicName = scenicName
|
||||||
|
self.storeId = storeId
|
||||||
|
self.storeName = storeName
|
||||||
|
self.scenicId = scenicId
|
||||||
|
self.status = status
|
||||||
|
self.isCurrent = isCurrent
|
||||||
|
}
|
||||||
|
|
||||||
var id: String {
|
var id: String {
|
||||||
"\(accountType)_\(businessUserId)"
|
"\(accountType)_\(businessUserId)"
|
||||||
}
|
}
|
||||||
@@ -134,6 +167,11 @@ struct AccountSwitchAccount: Identifiable, Hashable {
|
|||||||
accountType == V9StoreUser.accountTypeValue
|
accountType == V9StoreUser.accountTypeValue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `status == 2` 表示门店身份已提交注销申请,登录前需要用户明确确认撤销。
|
||||||
|
var requiresDeregistrationConfirmation: Bool {
|
||||||
|
isStoreUser && status == 2
|
||||||
|
}
|
||||||
|
|
||||||
var accountTypeLabel: String {
|
var accountTypeLabel: String {
|
||||||
isStoreUser ? "门店账号" : "景区账号"
|
isStoreUser ? "门店账号" : "景区账号"
|
||||||
}
|
}
|
||||||
@@ -146,6 +184,12 @@ struct AccountSwitchAccount: Identifiable, Hashable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 冷静期门店身份的待确认登录信息;确认后继续调用 `set-user`,由后端自动撤销注销申请。
|
||||||
|
struct DeregistrationLoginConfirmation: Equatable {
|
||||||
|
let tempToken: String
|
||||||
|
let account: AccountSwitchAccount
|
||||||
|
}
|
||||||
|
|
||||||
/// 登录账号选择载荷,保存临时 token 和待用户选择的账号列表。
|
/// 登录账号选择载荷,保存临时 token 和待用户选择的账号列表。
|
||||||
struct AccountSelectionPayload: Equatable, Identifiable {
|
struct AccountSelectionPayload: Equatable, Identifiable {
|
||||||
let id = UUID()
|
let id = UUID()
|
||||||
@@ -285,6 +329,7 @@ struct V9StoreUser: Decodable, Equatable {
|
|||||||
let roleName: String
|
let roleName: String
|
||||||
let appRoleCode: String
|
let appRoleCode: String
|
||||||
let appRoleName: String
|
let appRoleName: String
|
||||||
|
let status: Int
|
||||||
let isCurrent: Bool
|
let isCurrent: Bool
|
||||||
|
|
||||||
var businessUserId: Int {
|
var businessUserId: Int {
|
||||||
@@ -308,6 +353,7 @@ struct V9StoreUser: Decodable, Equatable {
|
|||||||
storeId: storeId > 0 ? storeId : nil,
|
storeId: storeId > 0 ? storeId : nil,
|
||||||
storeName: storeName,
|
storeName: storeName,
|
||||||
scenicId: scenicId > 0 ? scenicId : nil,
|
scenicId: scenicId > 0 ? scenicId : nil,
|
||||||
|
status: status,
|
||||||
isCurrent: isCurrent
|
isCurrent: isCurrent
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -327,6 +373,7 @@ struct V9StoreUser: Decodable, Equatable {
|
|||||||
case roleName = "role_name"
|
case roleName = "role_name"
|
||||||
case appRoleCode = "app_role_code"
|
case appRoleCode = "app_role_code"
|
||||||
case appRoleName = "app_role_name"
|
case appRoleName = "app_role_name"
|
||||||
|
case status
|
||||||
case isCurrent = "is_current"
|
case isCurrent = "is_current"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,6 +393,7 @@ struct V9StoreUser: Decodable, Equatable {
|
|||||||
roleName = try container.decodeLossyString(forKey: .roleName)
|
roleName = try container.decodeLossyString(forKey: .roleName)
|
||||||
appRoleCode = try container.decodeLossyString(forKey: .appRoleCode)
|
appRoleCode = try container.decodeLossyString(forKey: .appRoleCode)
|
||||||
appRoleName = try container.decodeLossyString(forKey: .appRoleName)
|
appRoleName = try container.decodeLossyString(forKey: .appRoleName)
|
||||||
|
status = try container.decodeLossyInt(forKey: .status) ?? 1
|
||||||
isCurrent = try container.decodeLossyBool(forKey: .isCurrent) ?? false
|
isCurrent = try container.decodeLossyBool(forKey: .isCurrent) ?? false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -181,12 +181,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||||||
@objc private func handleUserDidLogin() {
|
@objc private func handleUserDidLogin() {
|
||||||
sessionExpiredDialog?.dismiss(animated: false)
|
sessionExpiredDialog?.dismiss(animated: false)
|
||||||
sessionExpiredDialog = nil
|
sessionExpiredDialog = nil
|
||||||
if AppStore.shared.session.isLoggedIn, AppStore.shared.session.accountType == .storeUser {
|
// v9 登录响应中的 store_users[].status 是登录是否需要注销确认的唯一依据。
|
||||||
deregistrationCoordinator?.check()
|
// 旧 account-deregister/status 仅供用户主动进入注销设置流程时查询,不能再拦截正常登录。
|
||||||
} else {
|
|
||||||
refreshRootForCurrentSession()
|
refreshRootForCurrentSession()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func handleAccountDidSwitch() {
|
@objc private func handleAccountDidSwitch() {
|
||||||
if AppStore.shared.session.accountType == .storeUser {
|
if AppStore.shared.session.accountType == .storeUser {
|
||||||
|
|||||||
@@ -164,6 +164,16 @@ final class AccountSelectionViewController: UIViewController, UITableViewDelegat
|
|||||||
|
|
||||||
@objc private func confirmTapped() {
|
@objc private func confirmTapped() {
|
||||||
guard canConfirm, let selectedAccount else { return }
|
guard canConfirm, let selectedAccount else { return }
|
||||||
|
if selectedAccount.requiresDeregistrationConfirmation {
|
||||||
|
present(
|
||||||
|
makeDeregistrationLoginAlert(
|
||||||
|
account: selectedAccount,
|
||||||
|
onConfirm: { [weak self] in self?.onConfirm(selectedAccount) }
|
||||||
|
),
|
||||||
|
animated: true
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
onConfirm(selectedAccount)
|
onConfirm(selectedAccount)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,6 +187,23 @@ final class AccountSelectionViewController: UIViewController, UITableViewDelegat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 创建冷静期账号登录确认弹窗,确认后 `set-user` 会由后端自动撤销原注销申请。
|
||||||
|
func makeDeregistrationLoginAlert(
|
||||||
|
account: AccountSwitchAccount,
|
||||||
|
onConfirm: @escaping () -> Void
|
||||||
|
) -> UIAlertController {
|
||||||
|
let accountName = account.title.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
let displayName = accountName.isEmpty ? "该门店账号" : "“\(accountName)”"
|
||||||
|
let alert = UIAlertController(
|
||||||
|
title: "该账号正在注销",
|
||||||
|
message: "\(displayName)已提交注销申请,目前处于冷静期。确认登录后,将自动撤销之前的注销申请。",
|
||||||
|
preferredStyle: .alert
|
||||||
|
)
|
||||||
|
alert.addAction(UIAlertAction(title: "暂不登录", style: .cancel))
|
||||||
|
alert.addAction(UIAlertAction(title: "确认登录", style: .default) { _ in onConfirm() })
|
||||||
|
return alert
|
||||||
|
}
|
||||||
|
|
||||||
/// 账号选择列表 Cell。
|
/// 账号选择列表 Cell。
|
||||||
private final class AccountSelectionCell: UITableViewCell {
|
private final class AccountSelectionCell: UITableViewCell {
|
||||||
static let reuseIdentifier = "AccountSelectionCell"
|
static let reuseIdentifier = "AccountSelectionCell"
|
||||||
|
|||||||
@@ -290,6 +290,8 @@ final class LoginViewController: BaseViewController {
|
|||||||
completeLogin(with: response, account: account)
|
completeLogin(with: response, account: account)
|
||||||
case .needsAccountSelection:
|
case .needsAccountSelection:
|
||||||
break
|
break
|
||||||
|
case let .needsDeregistrationConfirmation(confirmation):
|
||||||
|
presentDeregistrationLoginConfirmation(confirmation)
|
||||||
}
|
}
|
||||||
} catch is CancellationError {
|
} catch is CancellationError {
|
||||||
return
|
return
|
||||||
@@ -335,6 +337,31 @@ final class LoginViewController: BaseViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func presentDeregistrationLoginConfirmation(_ confirmation: DeregistrationLoginConfirmation) {
|
||||||
|
let alert = makeDeregistrationLoginAlert(
|
||||||
|
account: confirmation.account,
|
||||||
|
onConfirm: { [weak self] in
|
||||||
|
self?.confirmDeregistrationLogin(confirmation)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
present(alert, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func confirmDeregistrationLogin(_ confirmation: DeregistrationLoginConfirmation) {
|
||||||
|
Task {
|
||||||
|
showLoading()
|
||||||
|
defer { hideLoading() }
|
||||||
|
do {
|
||||||
|
let response = try await viewModel.confirmDeregistrationLogin(confirmation, authAPI: authAPI)
|
||||||
|
completeLogin(with: response, account: confirmation.account)
|
||||||
|
} catch is CancellationError {
|
||||||
|
return
|
||||||
|
} catch {
|
||||||
|
showToast(error.localizedDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func completeLogin(with response: V9AuthResponse, account: AccountSwitchAccount) {
|
private func completeLogin(with response: V9AuthResponse, account: AccountSwitchAccount) {
|
||||||
AuthSessionHelper.completeLogin(
|
AuthSessionHelper.completeLogin(
|
||||||
with: response,
|
with: response,
|
||||||
|
|||||||
@@ -130,6 +130,28 @@ final class LoginViewModel {
|
|||||||
guard let payload = pendingAccountSelection, payload.hasTempToken else {
|
guard let payload = pendingAccountSelection, payload.hasTempToken else {
|
||||||
throw LoginFlowError.missingToken
|
throw LoginFlowError.missingToken
|
||||||
}
|
}
|
||||||
|
let response = try await setUser(account, tempToken: payload.tempToken, authAPI: authAPI)
|
||||||
|
pendingAccountSelection = nil
|
||||||
|
notifyStateChange()
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 用户确认登录冷静期账号后继续换取正式 token;`set-user` 成功即由后端撤销原注销申请。
|
||||||
|
func confirmDeregistrationLogin(
|
||||||
|
_ confirmation: DeregistrationLoginConfirmation,
|
||||||
|
authAPI: AuthAPI
|
||||||
|
) async throws -> V9AuthResponse {
|
||||||
|
try await setUser(confirmation.account, tempToken: confirmation.tempToken, authAPI: authAPI)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setUser(
|
||||||
|
_ account: AccountSwitchAccount,
|
||||||
|
tempToken: String,
|
||||||
|
authAPI: AuthAPI
|
||||||
|
) async throws -> V9AuthResponse {
|
||||||
|
guard !tempToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
||||||
|
throw LoginFlowError.missingToken
|
||||||
|
}
|
||||||
guard account.businessUserId > 0 else {
|
guard account.businessUserId > 0 else {
|
||||||
throw LoginFlowError.invalidAccount
|
throw LoginFlowError.invalidAccount
|
||||||
}
|
}
|
||||||
@@ -144,12 +166,10 @@ final class LoginViewModel {
|
|||||||
notifyStateChange()
|
notifyStateChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
let response = try await authAPI.setUser(account.toSetUserRequest(), tokenOverride: payload.tempToken)
|
let response = try await authAPI.setUser(account.toSetUserRequest(), tokenOverride: tempToken)
|
||||||
guard !response.token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
guard !response.token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
||||||
throw LoginFlowError.missingToken
|
throw LoginFlowError.missingToken
|
||||||
}
|
}
|
||||||
pendingAccountSelection = nil
|
|
||||||
notifyStateChange()
|
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,6 +198,11 @@ final class LoginViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if accounts.count == 1, let account = accounts.first {
|
if accounts.count == 1, let account = accounts.first {
|
||||||
|
if account.requiresDeregistrationConfirmation {
|
||||||
|
return .needsDeregistrationConfirmation(
|
||||||
|
DeregistrationLoginConfirmation(tempToken: token, account: account)
|
||||||
|
)
|
||||||
|
}
|
||||||
let finalResponse = try await authAPI.setUser(account.toSetUserRequest(), tokenOverride: token)
|
let finalResponse = try await authAPI.setUser(account.toSetUserRequest(), tokenOverride: token)
|
||||||
guard !finalResponse.token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
guard !finalResponse.token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
||||||
throw LoginFlowError.missingToken
|
throw LoginFlowError.missingToken
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ final class AuthModelsTests: XCTestCase {
|
|||||||
"scenic_name": "示例景区",
|
"scenic_name": "示例景区",
|
||||||
"store_id": 20,
|
"store_id": 20,
|
||||||
"store_name": "示例门店",
|
"store_name": "示例门店",
|
||||||
|
"status": 2,
|
||||||
"role_name": "店长",
|
"role_name": "店长",
|
||||||
"is_current": false
|
"is_current": false
|
||||||
}
|
}
|
||||||
@@ -63,6 +64,8 @@ final class AuthModelsTests: XCTestCase {
|
|||||||
XCTAssertEqual(response.accounts.map(\.businessUserId), [101, 201])
|
XCTAssertEqual(response.accounts.map(\.businessUserId), [101, 201])
|
||||||
XCTAssertEqual(response.accounts.map(\.subtitle), ["示例景区 · 摄影师", "示例景区 · 店长"])
|
XCTAssertEqual(response.accounts.map(\.subtitle), ["示例景区 · 摄影师", "示例景区 · 店长"])
|
||||||
XCTAssertEqual(response.accounts.map(\.realName), ["张三", "李四"])
|
XCTAssertEqual(response.accounts.map(\.realName), ["张三", "李四"])
|
||||||
|
XCTAssertEqual(response.storeUsers.first?.status, 2)
|
||||||
|
XCTAssertTrue(response.storeUsers.first?.toAccountSwitchAccount().requiresDeregistrationConfirmation == true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testV9AuthResponseDecodesMissingAccountListsAsEmpty() throws {
|
func testV9AuthResponseDecodesMissingAccountListsAsEmpty() throws {
|
||||||
@@ -137,6 +140,8 @@ final class AuthModelsTests: XCTestCase {
|
|||||||
XCTAssertEqual(user.toAccountSwitchAccount().subtitle, "示例景区 · 店长")
|
XCTAssertEqual(user.toAccountSwitchAccount().subtitle, "示例景区 · 店长")
|
||||||
XCTAssertEqual(user.toAccountSwitchAccount().realName, "张三")
|
XCTAssertEqual(user.toAccountSwitchAccount().realName, "张三")
|
||||||
XCTAssertEqual(user.toAccountSwitchAccount().toSetUserRequest(), SetUserRequest(storeUserId: 201))
|
XCTAssertEqual(user.toAccountSwitchAccount().toSetUserRequest(), SetUserRequest(storeUserId: 201))
|
||||||
|
XCTAssertEqual(user.status, 1)
|
||||||
|
XCTAssertFalse(user.toAccountSwitchAccount().requiresDeregistrationConfirmation)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testV9AccountModelsIgnoreBackendIdentifiersAndUseRealNameFallback() throws {
|
func testV9AccountModelsIgnoreBackendIdentifiersAndUseRealNameFallback() throws {
|
||||||
|
|||||||
@@ -48,6 +48,42 @@ final class LoginViewModelTests: XCTestCase {
|
|||||||
XCTAssertEqual(session.requests.map { $0.url?.path }, ["/api/app/v9/login", "/api/app/v9/set-user"])
|
XCTAssertEqual(session.requests.map { $0.url?.path }, ["/api/app/v9/login", "/api/app/v9/set-user"])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 单一门店身份处于注销冷静期时,必须先返回确认状态,不能自动调用 set-user 撤销申请。
|
||||||
|
func testSingleCoolingOffStoreAccountRequiresConfirmationBeforeSetUser() async throws {
|
||||||
|
let login = Data(#"{"code":100000,"data":{"token":"temporary","scenic_users":[],"store_users":[{"store_user_id":101,"store_name":"待注销门店","status":2}]}}"#.utf8)
|
||||||
|
let selected = Data(#"{"code":100000,"data":{"token":"business","scenic_users":[],"store_users":[]}}"#.utf8)
|
||||||
|
let session = MockURLSession(responses: [login, selected])
|
||||||
|
let viewModel = LoginViewModel()
|
||||||
|
viewModel.updateAccount("13900000001")
|
||||||
|
viewModel.updatePassword("test-password")
|
||||||
|
let api = AuthAPI(client: APIClient(environment: .testing, session: session))
|
||||||
|
|
||||||
|
let resolution = try await viewModel.login(authAPI: api)
|
||||||
|
|
||||||
|
guard case let .needsDeregistrationConfirmation(confirmation) = resolution else {
|
||||||
|
return XCTFail("冷静期门店身份应等待用户确认")
|
||||||
|
}
|
||||||
|
XCTAssertEqual(confirmation.account.status, 2)
|
||||||
|
XCTAssertEqual(session.requests.map { $0.url?.path }, ["/api/app/v9/login"])
|
||||||
|
|
||||||
|
let response = try await viewModel.confirmDeregistrationLogin(confirmation, authAPI: api)
|
||||||
|
|
||||||
|
XCTAssertEqual(response.token, "business")
|
||||||
|
XCTAssertEqual(session.requests.map { $0.url?.path }, ["/api/app/v9/login", "/api/app/v9/set-user"])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 冷静期确认弹窗需明确告知登录将撤销注销申请,并提供保留申请的取消入口。
|
||||||
|
func testDeregistrationLoginAlertExplainsAutomaticCancellation() {
|
||||||
|
let alert = makeDeregistrationLoginAlert(
|
||||||
|
account: reentryAccount(isStore: true, status: 2),
|
||||||
|
onConfirm: {}
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(alert.title, "该账号正在注销")
|
||||||
|
XCTAssertTrue(alert.message?.contains("自动撤销之前的注销申请") == true)
|
||||||
|
XCTAssertEqual(alert.actions.map(\.title), ["暂不登录", "确认登录"])
|
||||||
|
}
|
||||||
|
|
||||||
/// 普通门店身份选择直接回调,不再无条件弹出注销提示。
|
/// 普通门店身份选择直接回调,不再无条件弹出注销提示。
|
||||||
func testSelectingStoreIdentityContinuesWithoutDeregistrationPrompt() throws {
|
func testSelectingStoreIdentityContinuesWithoutDeregistrationPrompt() throws {
|
||||||
let account = reentryAccount(isStore: true)
|
let account = reentryAccount(isStore: true)
|
||||||
@@ -157,7 +193,7 @@ final class LoginViewModelTests: XCTestCase {
|
|||||||
XCTAssertNil(controller.presentedViewController)
|
XCTAssertNil(controller.presentedViewController)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func reentryAccount(isStore: Bool) -> AccountSwitchAccount {
|
private func reentryAccount(isStore: Bool, status: Int = 1) -> AccountSwitchAccount {
|
||||||
AccountSwitchAccount(
|
AccountSwitchAccount(
|
||||||
accountType: isStore ? "store_user" : "scenic_user",
|
accountType: isStore ? "store_user" : "scenic_user",
|
||||||
businessUserId: 101,
|
businessUserId: 101,
|
||||||
@@ -170,6 +206,7 @@ final class LoginViewModelTests: XCTestCase {
|
|||||||
storeId: isStore ? 25 : nil,
|
storeId: isStore ? 25 : nil,
|
||||||
storeName: isStore ? "测试门店" : "",
|
storeName: isStore ? "测试门店" : "",
|
||||||
scenicId: 10,
|
scenicId: 10,
|
||||||
|
status: status,
|
||||||
isCurrent: false
|
isCurrent: false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,8 +83,24 @@ final class StoreAccountDeregistrationRootTests: XCTestCase {
|
|||||||
XCTAssertEqual(context.resumeCount, 1)
|
XCTAssertEqual(context.resumeCount, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 新登录得到身份凭证后仍先核验,普通页面前台恢复不追加查询。
|
/// 新登录只以 v9 的门店 status 判定;旧注销状态接口和本地未决记录都不能再拦截 status 为正常的登录。
|
||||||
func testLoginChecksStatusBeforeEnteringBusiness() async {
|
func testNormalLoginRestoresBusinessWithoutLegacyDeregistrationCheck() async {
|
||||||
|
let context = DeregistrationRootContext()
|
||||||
|
defer { context.cleanUp() }
|
||||||
|
context.service.readError = APIError.networkFailed("旧注销查询不应影响 v9 正常登录")
|
||||||
|
context.startRestoringSession()
|
||||||
|
|
||||||
|
XCTAssertNil(context.coordinator.accessController)
|
||||||
|
XCTAssertFalse(context.coordinator.isChecking)
|
||||||
|
XCTAssertFalse(GlobalLoadingManager.shared.isShowing)
|
||||||
|
XCTAssertFalse(context.bindingSuspended)
|
||||||
|
XCTAssertEqual(context.createdRoots, 1)
|
||||||
|
XCTAssertEqual(context.resumeCount, 1)
|
||||||
|
XCTAssertEqual(context.service.statusCount, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 旧核验入口仅在业务限制等已知受限事件发生时使用,不是正常登录路径的一部分。
|
||||||
|
func testExplicitRestrictionCheckStillKeepsOriginalPageUntilResult() async {
|
||||||
let context = DeregistrationRootContext()
|
let context = DeregistrationRootContext()
|
||||||
defer { context.cleanUp() }
|
defer { context.cleanUp() }
|
||||||
context.service.holdNextRead = true
|
context.service.holdNextRead = true
|
||||||
|
|||||||
Reference in New Issue
Block a user