Add APNs push notification registration and payload routing.
Introduce AppDelegate, explicit Info.plist and entitlements, and wire PushNotificationManager into login lifecycle for device token upload and notification handling. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
133
suixinkanTests/PushNotificationTests.swift
Normal file
133
suixinkanTests/PushNotificationTests.swift
Normal file
@ -0,0 +1,133 @@
|
||||
//
|
||||
// PushNotificationTests.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
// Created by Codex on 2026/6/26.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
/// 推送链路测试,覆盖 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)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user