// // PushNotificationTests.swift // suixinkanTests // // Created by Codex on 2026/6/26. // import Foundation import XCTest @testable import suixinkan_ios /// 推送链路测试,覆盖 APNs token、payload 解析、token 上报和通知路由。 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 router = AppRouter() let manager = pushManager(router: router) manager.handleRemoteNotification(PushPayload(userInfo: ["route": "payment_qr"])) XCTAssertEqual(router.selectedTab, .home) XCTAssertEqual(router.router(for: .home).path, [.home(.paymentCollection)]) router.router(for: .home).reset() manager.handleRemoteNotification(PushPayload(userInfo: ["route": "task_management"])) XCTAssertEqual(router.router(for: .home).path, [.home(.taskManagement)]) router.router(for: .home).reset() manager.handleRemoteNotification(PushPayload(userInfo: ["uri": "/scenic-queue"])) XCTAssertEqual(router.router(for: .home).path, [.home(.queueManagement)]) router.router(for: .home).reset() manager.handleRemoteNotification(PushPayload(userInfo: ["title": "未知通知"])) XCTAssertEqual(router.router(for: .home).path, [.home(.messageCenter)]) } func testPushManagerRoutesOrdersEntry() { let router = AppRouter() let manager = pushManager(router: router) manager.handleRemoteNotification(PushPayload(userInfo: ["route": "photographer_orders"])) XCTAssertEqual(router.selectedTab, .orders) XCTAssertEqual(router.selectedOrdersEntry, .storeOrders) manager.handleRemoteNotification(PushPayload(userInfo: ["route": "verification_order"])) XCTAssertEqual(router.selectedTab, .orders) XCTAssertEqual(router.selectedOrdersEntry, .verificationOrders) } private func pushManager(router: AppRouter) -> PushNotificationManager { let session = AppSession() let manager = PushNotificationManager() manager.configure( api: PushAPI(client: APIClient(session: PushRecordingURLSession())), session: session, router: router ) return manager } } 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) }) }