Integrate高德 SDK with simulator-safe build flags, add map views for operating area and punch points, refactor main tabs and key feature screens to UIKit with Diffable lists, and document Swift concurrency defaults in AGENTS.md. Co-authored-by: Cursor <cursoragent@cursor.com>
135 lines
5.0 KiB
Swift
135 lines
5.0 KiB
Swift
//
|
||
// 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 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)
|
||
})
|
||
}
|