// // PermissionApplyViewModelTests.swift // suixinkanTests // import XCTest @testable import suixinkan @MainActor /// 权限申请 ViewModel 测试。 final class PermissionApplyViewModelTests: XCTestCase { private var appStore: AppStore! private var defaults: UserDefaults! override func setUp() { defaults = UserDefaults(suiteName: "PermissionApplyViewModelTests")! defaults.removePersistentDomain(forName: "PermissionApplyViewModelTests") appStore = AppStore(defaults: defaults) appStore.session.configureTestAccount() appStore.permissions.saveRolePermissionList([ RolePermissionResponse( role: RoleInfo(id: 10, name: "摄影师", roleCode: "PHOTOGRAPHER", notes: "拍摄"), scenic: [ScenicInfo(id: 100, name: "已有景区", status: 1)] ), ]) } override func tearDown() { appStore.logout() super.tearDown() } func testExistingScenicIsDisabledAfterLoad() async throws { let scenicJSON = """ {"code":100000,"msg":"success","data":{"total":2,"list":[{"id":100,"name":"已有景区"},{"id":101,"name":"新景区"}]}} """.data(using: .utf8)! let api = HomeAPI(client: APIClient(environment: .testing, session: MockURLSession(responses: [scenicJSON]))) let viewModel = PermissionApplyViewModel(appStore: appStore) viewModel.selectRole(RoleOption(id: 10, name: "摄影师", description: "拍摄", isSelected: true)) await viewModel.loadAvailableScenics(api: api) let existing = viewModel.availableScenics.first { $0.id == 100 } let fresh = viewModel.availableScenics.first { $0.id == 101 } XCTAssertEqual(existing?.isDisabled, true) XCTAssertEqual(existing?.isSelected, true) XCTAssertEqual(fresh?.isDisabled, false) if let existing { viewModel.toggleScenic(existing) } XCTAssertTrue(viewModel.selectedScenicIds.isEmpty) } func testSubmitValidationRequiresRoleAndScenic() async { let api = HomeAPI(client: APIClient(environment: .testing, session: MockURLSession(responses: []))) let viewModel = PermissionApplyViewModel(appStore: appStore) var message: String? viewModel.onShowMessage = { message = $0 } await viewModel.submit(api: api) XCTAssertEqual(message, "请选择景区和角色") XCTAssertFalse(viewModel.canSubmit) viewModel.selectRole(RoleOption(id: 10, name: "摄影师", description: "", isSelected: true)) await viewModel.submit(api: api) XCTAssertEqual(message, "请选择景区和角色") XCTAssertFalse(viewModel.canSubmit) } func testSubmitSuccessUsesAndroidPayloadAndCompletion() async throws { let scenicJSON = """ {"code":100000,"msg":"success","data":{"total":1,"list":[{"id":101,"name":"新景区"}]}} """.data(using: .utf8)! let submitJSON = """ {"code":100000,"msg":"success","data":{"code":"AP999"}} """.data(using: .utf8)! let session = MockURLSession(responses: [scenicJSON, submitJSON]) let api = HomeAPI(client: APIClient(environment: .testing, session: session)) let viewModel = PermissionApplyViewModel(appStore: appStore) viewModel.selectRole(RoleOption(id: 10, name: "摄影师", description: "", isSelected: true)) await viewModel.loadAvailableScenics(api: api) if let scenic = viewModel.availableScenics.first(where: { $0.id == 101 }) { viewModel.toggleScenic(scenic) } var didSubmit = false viewModel.onSubmitSuccess = { didSubmit = true } XCTAssertTrue(viewModel.canSubmit) await viewModel.submit(api: api) XCTAssertTrue(didSubmit) XCTAssertFalse(viewModel.isSubmitting) XCTAssertEqual(session.requests.last?.url?.path, "/api/yf-handset-app/photog/role-apply/submit") let body = try XCTUnwrap(session.requests.last?.httpBody) let object = try XCTUnwrap(JSONSerialization.jsonObject(with: body) as? [String: Any]) XCTAssertEqual(object["role_id"] as? Int, 10) XCTAssertEqual(object["scenic_id"] as? [Int], [101]) } func testSubmitFailureRestoresEnabledButtonAndKeepsSelection() async throws { let scenicJSON = """ {"code":100000,"msg":"success","data":{"total":1,"list":[{"id":101,"name":"新景区"}]}} """.data(using: .utf8)! let failureJSON = #"{"code":100001,"msg":"failed","data":null}"#.data(using: .utf8)! let api = HomeAPI( client: APIClient( environment: .testing, session: MockURLSession(responses: [scenicJSON, failureJSON]) ) ) let viewModel = PermissionApplyViewModel(appStore: appStore) viewModel.selectRole(RoleOption(id: 10, name: "摄影师", description: "", isSelected: true)) await viewModel.loadAvailableScenics(api: api) viewModel.toggleScenic(try XCTUnwrap(viewModel.availableScenics.first)) var message: String? viewModel.onShowMessage = { message = $0 } await viewModel.submit(api: api) XCTAssertEqual(message, "提交失败,请稍后重试") XCTAssertFalse(viewModel.isSubmitting) XCTAssertTrue(viewModel.canSubmit) XCTAssertEqual(viewModel.selectedScenicIds, [101]) } func testSwitchingRoleClearsScenicSelectionAndLoadedOptions() async throws { let scenicJSON = """ {"code":100000,"msg":"success","data":{"total":1,"list":[{"id":101,"name":"新景区"}]}} """.data(using: .utf8)! let api = HomeAPI(client: APIClient(environment: .testing, session: MockURLSession(responses: [scenicJSON]))) let viewModel = PermissionApplyViewModel(appStore: appStore) viewModel.selectRole(RoleOption(id: 10, name: "摄影师", description: "", isSelected: true)) await viewModel.loadAvailableScenics(api: api) viewModel.toggleScenic(try XCTUnwrap(viewModel.availableScenics.first)) viewModel.selectRole(RoleOption(id: 11, name: "门店管理员", description: "", isSelected: true)) XCTAssertEqual(viewModel.selectedRoleId, 11) XCTAssertTrue(viewModel.selectedScenicIds.isEmpty) XCTAssertTrue(viewModel.availableScenics.isEmpty) } func testAvailableRolesAreDeduplicatedByRoleID() { appStore.permissions.saveRolePermissionList([ RolePermissionResponse( role: RoleInfo(id: 10, name: "摄影师", roleCode: "PHOTOGRAPHER", notes: "拍摄"), scenic: [ScenicInfo(id: 100, name: "景区A", status: 1)] ), RolePermissionResponse( role: RoleInfo(id: 10, name: "摄影师", roleCode: "PHOTOGRAPHER", notes: "拍摄"), scenic: [ScenicInfo(id: 101, name: "景区B", status: 1)] ), ]) let viewModel = PermissionApplyViewModel(appStore: appStore) XCTAssertEqual(viewModel.availableRoles.map(\.id), [10]) XCTAssertEqual(viewModel.rolePermissionList.count, 2) } func testScenicSearchAndDeletionConfirmation() async throws { let scenicJSON = """ {"code":100000,"msg":"success","data":{"total":3,"list":[{"id":100,"name":"已有景区"},{"id":101,"name":"西湖景区"},{"id":102,"name":"东湖景区"}]}} """.data(using: .utf8)! let api = HomeAPI(client: APIClient(environment: .testing, session: MockURLSession(responses: [scenicJSON]))) let viewModel = PermissionApplyViewModel(appStore: appStore) viewModel.selectRole(RoleOption(id: 10, name: "摄影师", description: "", isSelected: true)) await viewModel.loadAvailableScenics(api: api) viewModel.updateScenicSearchQuery("西湖") XCTAssertEqual(viewModel.filteredScenics.map(\.id), [101]) let scenic = try XCTUnwrap(viewModel.filteredScenics.first) viewModel.toggleScenic(scenic) XCTAssertTrue(viewModel.canSubmit) viewModel.removeSelectedScenic(id: scenic.id, name: scenic.name) XCTAssertEqual(viewModel.pendingDeletion, .scenic(id: 101, name: "西湖景区")) viewModel.cancelDeletion() XCTAssertEqual(viewModel.selectedScenicIds, [101]) viewModel.removeSelectedScenic(id: scenic.id, name: scenic.name) viewModel.confirmDeletion() XCTAssertTrue(viewModel.selectedScenicIds.isEmpty) XCTAssertFalse(viewModel.canSubmit) } func testRoleDeletionRequiresConfirmationAndClearsScenics() async throws { let scenicJSON = """ {"code":100000,"msg":"success","data":{"total":1,"list":[{"id":101,"name":"新景区"}]}} """.data(using: .utf8)! let api = HomeAPI(client: APIClient(environment: .testing, session: MockURLSession(responses: [scenicJSON]))) let viewModel = PermissionApplyViewModel(appStore: appStore) viewModel.selectRole(RoleOption(id: 10, name: "摄影师", description: "", isSelected: true)) await viewModel.loadAvailableScenics(api: api) viewModel.toggleScenic(try XCTUnwrap(viewModel.availableScenics.first)) viewModel.clearRoleSelection() XCTAssertEqual(viewModel.pendingDeletion, .role(name: "摄影师")) XCTAssertEqual(viewModel.selectedRoleId, 10) viewModel.confirmDeletion() XCTAssertNil(viewModel.selectedRoleId) XCTAssertTrue(viewModel.selectedScenicIds.isEmpty) XCTAssertTrue(viewModel.availableScenics.isEmpty) } func testExistingRoleListAndScenicDialogState() { let viewModel = PermissionApplyViewModel(appStore: appStore) XCTAssertEqual(viewModel.rolePermissionList.count, 1) XCTAssertEqual(viewModel.availableRoles.first?.description, "拍摄") let scenics = viewModel.rolePermissionList[0].scenic viewModel.showScenicList(roleName: "摄影师", scenics: scenics) XCTAssertTrue(viewModel.showScenicListDialog) XCTAssertEqual(viewModel.dialogRoleName, "摄影师") XCTAssertEqual(viewModel.dialogScenicList.map(\.id), [100]) viewModel.hideScenicList() XCTAssertFalse(viewModel.showScenicListDialog) } }