Files
multi-simadmin/apps/ios/Tests/MultiSimAdminTests/LogCenterTests.swift
T
chick e01a105645 feat(ios): simplify navigation and ship 0.2.0
Move diagnostics into settings, remove the app-local notification module, unify the global theme switcher, refine privacy copy and the instance detail experience, and rebuild the app icon.
2026-09-12 17:10:02 +08:00

207 lines
7.0 KiB
Swift

import XCTest
@testable import MultiSimAdmin
final class LogCenterTests: XCTestCase {
func testAggregatesDeliveryLogsConnectionsAndDiagnostics() async throws {
let store = MemoryLogStore()
let service = LogCenterService(
client: LogCenterTestClient(),
sessions: InstanceSessionStore(),
store: store
)
let instance = Self.instance()
let snapshot = try await service.refresh([instance])
XCTAssertEqual(snapshot.runtime.count, 0)
XCTAssertEqual(snapshot.counts[.delivery], 0)
XCTAssertEqual(store.runtimeStorage.count, 0)
XCTAssertEqual(snapshot.connections.count, 1)
XCTAssertEqual(snapshot.connections.first?.outcome, .success)
XCTAssertEqual(snapshot.connections.first?.state, "online")
XCTAssertEqual(store.connectionStorage.count, 1)
let diagnostic = try XCTUnwrap(snapshot.diagnostics.first)
XCTAssertEqual(diagnostic.id, instance.id)
XCTAssertEqual(diagnostic.state, .online)
XCTAssertEqual(diagnostic.featureStatus.map(\.state), [
.ready,
.ready,
.ready
])
XCTAssertEqual(diagnostic.connection?.total, 1)
XCTAssertEqual(diagnostic.connection?.availabilityPercent, 100)
}
func testRecordsUnauthorizedDeliveryLogsWithoutLosingConnectionHistory() async throws {
let store = MemoryLogStore()
let service = LogCenterService(
client: LogCenterTestClient(
),
sessions: InstanceSessionStore(),
store: store
)
let instance = Self.instance()
let snapshot = try await service.refresh([instance])
XCTAssertEqual(snapshot.runtime.count, 0)
let diagnostic = try XCTUnwrap(snapshot.diagnostics.first)
XCTAssertEqual(diagnostic.state, .online)
XCTAssertEqual(diagnostic.featureStatus.filter { $0.state == .ready }.count, 3)
XCTAssertEqual(store.runtimeStorage.count, 0)
XCTAssertEqual(store.connectionStorage.count, 1)
}
func testOfflineProbeBecomesFailedConnectionAndErrorLog() async throws {
let store = MemoryLogStore()
let service = LogCenterService(
client: LogCenterTestClient(connectionUnavailable: true),
sessions: InstanceSessionStore(),
store: store
)
let snapshot = try await service.refresh([Self.instance()])
XCTAssertEqual(snapshot.connections.first?.outcome, .failed)
XCTAssertEqual(snapshot.connections.first?.errorCode, "TIMEOUT")
XCTAssertTrue(snapshot.runtime.contains { $0.level == .error && $0.code == "TIMEOUT" })
let diagnostic = try XCTUnwrap(snapshot.diagnostics.first)
XCTAssertEqual(diagnostic.state, .offline)
XCTAssertEqual(diagnostic.healthStatus, "超时")
XCTAssertEqual(diagnostic.featureStatus.filter { $0.state == .unavailable }.count, 2)
}
@MainActor
func testViewModelFiltersRuntimeLogsAndPrunesHistory() async throws {
let repository = MemoryInstanceRepository()
let logStore = MemoryLogStore()
let service = LogCenterService(
client: LogCenterTestClient(),
sessions: InstanceSessionStore(),
store: logStore
)
let model = LogsViewModel(
repository: repository,
service: service,
store: logStore
)
let instance = try await repository.create(InstanceInput(
name: "Log Node",
origin: URL(string: "http://192.168.1.20:3000")!,
password: nil
))
await model.refresh()
XCTAssertNotNil(model.snapshot)
XCTAssertEqual(model.filteredRuntime.count, 0)
XCTAssertEqual(model.filteredDiagnostics.count, 1)
XCTAssertEqual(model.offlineCount, 0)
model.selectedInstanceID = instance.id
model.searchText = "offline"
model.selectedLevel = .error
XCTAssertEqual(model.filteredRuntime.count, 0)
XCTAssertNil(model.filteredRuntime.first)
let removed = await model.pruneConnections(olderThanDays: 7)
XCTAssertEqual(removed, 0)
}
private static func instance() -> Instance {
Instance(
id: UUID(),
name: "Log Node",
origin: URL(string: "http://192.168.1.20:3000")!,
configRevision: 1,
createdAt: Date(),
updatedAt: Date()
)
}
}
private final class MemoryLogStore: LogStore, @unchecked Sendable {
private let lock = NSLock()
private var runtimeItems: [RuntimeLogEntry] = []
private var connectionItems: [ConnectionLogEntry] = []
var runtimeStorage: [RuntimeLogEntry] {
lock.withLock { runtimeItems }
}
var connectionStorage: [ConnectionLogEntry] {
lock.withLock { connectionItems }
}
func runtimeLogs() async throws -> [RuntimeLogEntry] {
runtimeStorage
}
func connectionLogs() async throws -> [ConnectionLogEntry] {
connectionStorage
}
func append(
runtime: [RuntimeLogEntry],
connections: [ConnectionLogEntry]
) async throws {
lock.withLock {
var mergedRuntime = runtime
for existing in runtimeItems where !runtime.contains(where: { $0.id == existing.id }) {
mergedRuntime.append(existing)
}
runtimeItems = mergedRuntime.sorted { $0.occurredAt > $1.occurredAt }
var mergedConnections = connections
for existing in connectionItems
where !connections.contains(where: { $0.id == existing.id }) {
mergedConnections.append(existing)
}
connectionItems = mergedConnections.sorted { $0.observedAt > $1.observedAt }
}
}
func pruneConnections(before _: Date) async throws -> Int {
0
}
func clearRuntimeLogs() async throws -> Int {
0
}
}
private struct LogCenterTestClient: SimAdminClientProtocol {
var failingRoutes: Set<UpstreamRoute> = []
var connectionUnavailable = false
func send(_ route: UpstreamRoute, baseURL: URL, cookie: String?) async throws -> UpstreamResponse {
if connectionUnavailable {
throw UpstreamError.timeout
}
if failingRoutes.contains(route) {
throw UpstreamError.unauthorized
}
switch route {
case .health:
return Self.response(#"{"status":"UP","version":"1.2.3","platform":"linux"}"#)
case .device:
return Self.response(
#"{"status":"ok","data":{"model":"UFI-003","battery":82,"temperature":42}}"#
)
case .connectivity:
return Self.response(#"{"status":"ok","data":{"connected":true}}"#)
default:
return UpstreamResponse(status: 404, data: Data("{}".utf8))
}
}
private static func response(_ json: String) -> UpstreamResponse {
UpstreamResponse(status: 200, data: Data(json.utf8))
}
}