// // PushNotificationTests.swift // suixinkanTests // // Created by Codex on 2026/6/26. // import Foundation import XCTest @testable import suixinkan_ios /// 推送链路测试,覆盖 APNs token、payload 解析、token 上报和通知路由。 @MainActor final class PushNotificationTests: XCTestCase { func testAPNsDeviceTokenUsesLowercaseHexString() { let token = APNsDeviceToken.hexString(from: Data([0x00, 0x0f, 0x10, 0xab, 0xff])) XCTAssertEqual(token, "000f10abff") } func testPushPayloadRoutesTopLevelFields() { XCTAssertEqual(PushPayload(userInfo: ["type": "1"]).route, .payment) XCTAssertEqual(PushPayload(userInfo: ["route": "verification_order"]).route, .verificationOrder) XCTAssertEqual(PushPayload(userInfo: ["uri": "/scenic-queue"]).route, .queue) XCTAssertEqual(PushPayload(userInfo: ["action": "task_management"]).route, .task) XCTAssertEqual(PushPayload(userInfo: ["route": "photographer_orders"]).route, .order) } func testPushPayloadRoutesNestedJSONString() { let payload = PushPayload(userInfo: [ "extras": #"{"data":{"uri":"message_center","title":"系统消息"}}"# ]) XCTAssertEqual(payload.route, .messageCenter) } func testPushPayloadRoutesNestedDictionaryAndChineseKeywords() { let payload = PushPayload(userInfo: [ "data": [ "extra": [ "action": "叫号提醒" ] ] ]) XCTAssertEqual(payload.route, .queue) } func testPushPayloadFallsBackToMessageCenter() { let payload = PushPayload(userInfo: ["title": "系统消息"]) XCTAssertEqual(payload.route, .messageCenter) } func testRegisterPushTokenUsesCurrentBackendEndpointAndLegacyQueryName() async throws { let session = PushRecordingURLSession() let api = PushAPI(client: APIClient(session: session)) try await api.registerJPushId("000f10abff") let request = try XCTUnwrap(session.requests.first) XCTAssertEqual(request.httpMethod, "POST") XCTAssertEqual(request.url?.path, "/api/app/user/register-jpush-id") XCTAssertEqual(queryItems(from: request)["jpush_reg_id"], "000f10abff") } func testPushManagerRoutesHomeDestinationsThroughHomeNavigationStack() { let services = AppServices(apiClient: APIClient(session: PushRecordingURLSession())) let host = PushNavigationHost(services: services) let manager = pushManager(navigator: services.appNavigator) services.appNavigator.attach(host: host) manager.handleRemoteNotification(PushPayload(userInfo: ["route": "payment_qr"])) XCTAssertEqual(host.selectedTab, .home) XCTAssertTrue(host.navigationController(for: .home)?.viewControllers.last is PaymentCollectionViewController) manager.handleRemoteNotification(PushPayload(userInfo: ["route": "task_management"])) XCTAssertTrue(host.navigationController(for: .home)?.viewControllers.last is TaskManagementViewController) XCTAssertEqual(host.navigationController(for: .home)?.viewControllers.count, 2) manager.handleRemoteNotification(PushPayload(userInfo: ["uri": "/scenic-queue"])) XCTAssertTrue(host.navigationController(for: .home)?.viewControllers.last is QueueManagementViewController) XCTAssertEqual(host.navigationController(for: .home)?.viewControllers.count, 2) manager.handleRemoteNotification(PushPayload(userInfo: ["title": "未知通知"])) XCTAssertTrue(host.navigationController(for: .home)?.viewControllers.last is MessageCenterViewController) XCTAssertEqual(host.navigationController(for: .home)?.viewControllers.count, 2) } func testPushManagerRoutesOrdersEntry() { let services = AppServices(apiClient: APIClient(session: PushRecordingURLSession())) let host = PushNavigationHost(services: services) let manager = pushManager(navigator: services.appNavigator) services.appNavigator.attach(host: host) manager.handleRemoteNotification(PushPayload(userInfo: ["route": "photographer_orders"])) XCTAssertEqual(host.selectedTab, .orders) XCTAssertEqual(host.appliedOrdersEntry, .storeOrders) manager.handleRemoteNotification(PushPayload(userInfo: ["route": "verification_order"])) XCTAssertEqual(host.selectedTab, .orders) XCTAssertEqual(host.appliedOrdersEntry, .verificationOrders) } private func pushManager(navigator: AppNavigator) -> PushNotificationManager { let session = AppSession() let manager = PushNotificationManager() manager.configure( api: PushAPI(client: APIClient(session: PushRecordingURLSession())), session: session, navigator: navigator ) return manager } } /// 推送路由单测专用导航宿主。 @MainActor private final class PushNavigationHost: AppNavigationHost { private var controllers: [AppTab: TabNavigationController] = [:] var selectedTab: AppTab = .home var appliedOrdersEntry: OrdersEntry? /// 初始化所有 Tab 导航栈。 init(services: AppServices) { for tab in AppTab.allCases { controllers[tab] = TabNavigationController(tab: tab, services: services) } } /// 选中指定主 Tab。 func selectTab(_ tab: AppTab) { selectedTab = tab } /// 返回指定 Tab 的导航控制器。 func navigationController(for tab: AppTab) -> TabNavigationController? { controllers[tab] } /// 返回当前选中 Tab 的导航控制器。 func selectedNavigationController() -> TabNavigationController? { controllers[selectedTab] } /// 重置全部 Tab 导航栈。 func resetAllStacks() { for controller in controllers.values { controller.popToRootViewController(animated: false) } selectedTab = .home } /// 记录订单入口状态。 func applyOrdersEntry(_ entry: OrdersEntry, scannedCode: String?) { appliedOrdersEntry = entry } } private final class PushRecordingURLSession: URLSessionProtocol { private(set) var requests: [URLRequest] = [] func data(for request: URLRequest) async throws -> (Data, URLResponse) { requests.append(request) let data = #"{"code":100000,"msg":"success","data":{}}"#.data(using: .utf8)! let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! return (data, response) } } private func queryItems(from request: URLRequest) -> [String: String] { guard let url = request.url, let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return [:] } return Dictionary(uniqueKeysWithValues: (components.queryItems ?? []).compactMap { item in guard let value = item.value else { return nil } return (item.name, value) }) }