feat(ios): add cross-instance notification center

This commit is contained in:
chick
2026-09-09 01:13:45 +08:00
parent 84ec3678c0
commit cf187e42fa
9 changed files with 1114 additions and 3 deletions
@@ -0,0 +1,91 @@
import XCTest
@testable import MultiSimAdmin
@MainActor
final class NotificationsViewModelTests: XCTestCase {
func testRefreshLoadsInstancesAndAggregatedOverview() async throws {
let repository = MemoryInstanceRepository()
let client = NotificationViewModelClient()
let service = NotificationService(client: client, sessions: InstanceSessionStore())
let model = NotificationsViewModel(repository: repository, service: service)
_ = try await repository.create(InstanceInput(
name: "Notification Node",
origin: URL(string: "http://192.168.1.20:3000")!,
tags: [],
password: nil
))
await model.refresh()
XCTAssertEqual(model.instances.count, 1)
XCTAssertEqual(model.overview?.devices.first?.state, .ready)
XCTAssertEqual(model.overview?.queue.items.first?.id, "queue-1")
XCTAssertNil(model.errorMessage)
}
func testRetryQueueTargetsOwningInstance() async throws {
let repository = MemoryInstanceRepository()
let client = NotificationViewModelClient()
let service = NotificationService(client: client, sessions: InstanceSessionStore())
let model = NotificationsViewModel(repository: repository, service: service)
_ = try await repository.create(InstanceInput(
name: "Notification Node",
origin: URL(string: "http://192.168.1.20:3000")!,
tags: [],
password: nil
))
await model.refresh()
let item = try XCTUnwrap(model.overview?.queue.items.first)
await model.retryQueue(item)
XCTAssertEqual(client.routes.last, .notificationRetryQueue(id: "queue-1"))
}
func testRetryAllTargetsEveryInstance() async throws {
let repository = MemoryInstanceRepository()
let client = NotificationViewModelClient()
let service = NotificationService(client: client, sessions: InstanceSessionStore())
let model = NotificationsViewModel(repository: repository, service: service)
_ = try await repository.create(InstanceInput(
name: "First",
origin: URL(string: "http://192.168.1.20:3000")!,
tags: [],
password: nil
))
_ = try await repository.create(InstanceInput(
name: "Second",
origin: URL(string: "http://192.168.1.21:3000")!,
tags: [],
password: nil
))
await model.refresh()
await model.retryAll()
XCTAssertEqual(client.routes.filter { $0 == .notificationRetryAllQueue }.count, 2)
}
}
private final class NotificationViewModelClient: SimAdminClientProtocol, @unchecked Sendable {
var routes: [UpstreamRoute] = []
func send(_ route: UpstreamRoute, baseURL: URL, cookie: String?) async throws -> UpstreamResponse {
routes.append(route)
switch route {
case .notificationConfig:
return Self.response(#"{"status":"ok","data":{"channels":[{"type":"webhook","enabled":true}]}}"#)
case .notificationLogs:
return Self.response(#"{"status":"ok","data":{"total":1,"logs":[{"id":"log-1","event_type":"system","status":"success","created_at":"2026-09-08T10:00:00Z"}]}}"#)
case .notificationQueue:
return Self.response(#"{"status":"ok","data":{"total":1,"items":[{"id":"queue-1","event_type":"system","status":"failed","created_at":"2026-09-08T10:00:00Z"}]}}"#)
default:
return Self.response(#"{"status":"ok"}"#)
}
}
private static func response(_ json: String) -> UpstreamResponse {
UpstreamResponse(status: 200, data: Data(json.utf8))
}
}