Files
multi-simadmin/apps/ios/Tests/MultiSimAdminTests/NotificationServiceTests.swift
T

164 lines
6.4 KiB
Swift

import XCTest
@testable import MultiSimAdmin
final class NotificationServiceTests: XCTestCase {
func testAggregatesInstanceConfigLogsAndQueue() async throws {
let client = NotificationTestClient()
let service = NotificationService(client: client, sessions: InstanceSessionStore())
let instance = Self.instance()
let overview = try await service.overview([instance])
XCTAssertEqual(client.routes, [
.notificationConfig,
.notificationLogs(limit: 50, offset: 0),
.notificationQueue(limit: 100)
])
XCTAssertEqual(overview.config.channelCount, 2)
XCTAssertEqual(overview.config.channelEnabled, 1)
XCTAssertEqual(overview.config.ruleCount, 1)
XCTAssertEqual(overview.config.ruleEnabled, 1)
XCTAssertEqual(overview.config.channelTypes.first?.type, "webhook")
XCTAssertEqual(overview.logs.total, 3)
XCTAssertEqual(overview.logs.success, 2)
XCTAssertEqual(overview.logs.failed, 1)
XCTAssertEqual(overview.queue.total, 2)
XCTAssertEqual(overview.queue.pending, 1)
XCTAssertEqual(overview.queue.failed, 1)
XCTAssertEqual(overview.queue.items.first?.id, "q-1")
XCTAssertEqual(overview.queue.items.first?.instanceID, instance.id)
XCTAssertEqual(overview.devices.first?.state, .ready)
}
func testQueueActionsValidateIDsAndUseUpstreamRoutes() async throws {
let client = NotificationTestClient()
let service = NotificationService(client: client, sessions: InstanceSessionStore())
let instance = Self.instance()
try await service.retryQueue(instance, id: "q_1-a.2")
try await service.deleteQueueItem(instance, id: "q_1-a.2")
XCTAssertEqual(client.routes.suffix(2), [
.notificationRetryQueue(id: "q_1-a.2"),
.notificationDeleteQueue(id: "q_1-a.2")
])
do {
try await service.retryQueue(instance, id: "bad id")
XCTFail("Invalid queue ID should fail.")
} catch {
XCTAssertEqual(error as? NotificationServiceError, .invalidInput)
}
}
func testReturnsStatefulOverviewWhenEveryInstanceIsUnauthorized() async throws {
let service = NotificationService(
client: UnauthorizedNotificationClient(),
sessions: InstanceSessionStore()
)
let instance = Self.instance()
let overview = try await service.overview([instance])
XCTAssertEqual(overview.devices, [NotificationDevice(
id: instance.id,
name: instance.name,
state: .unauthorized
)])
XCTAssertEqual(overview.config.channelCount, 0)
XCTAssertEqual(overview.logs.total, 0)
XCTAssertEqual(overview.queue.total, 0)
}
func testAggregatesChannelTypeSummariesInFirstSeenOrder() {
let first = NotificationConfigSummary(
channelCount: 2,
channelEnabled: 1,
ruleCount: 0,
ruleEnabled: 0,
channelTypes: [
NotificationChannelTypeSummary(type: "webhook", total: 1, enabled: 1),
NotificationChannelTypeSummary(type: "bark", total: 1, enabled: 0)
]
)
let second = NotificationConfigSummary(
channelCount: 2,
channelEnabled: 2,
ruleCount: 0,
ruleEnabled: 0,
channelTypes: [
NotificationChannelTypeSummary(type: "telegram", total: 1, enabled: 1),
NotificationChannelTypeSummary(type: "webhook", total: 1, enabled: 1)
]
)
let result = NotificationService.aggregateConfigs([first, second])
XCTAssertEqual(result.channelTypes.map(\.type), ["webhook", "bark", "telegram"])
let webhook = result.channelTypes.first { $0.type == "webhook" }
XCTAssertEqual(webhook, NotificationChannelTypeSummary(type: "webhook", total: 2, enabled: 2))
}
private static func instance() -> Instance {
Instance(
id: UUID(),
name: "Notification Node",
origin: URL(string: "http://192.168.1.20:3000")!,
configRevision: 1,
createdAt: Date(),
updatedAt: Date()
)
}
}
private struct UnauthorizedNotificationClient: SimAdminClientProtocol {
func send(_ route: UpstreamRoute, baseURL: URL, cookie: String?) async throws -> UpstreamResponse {
throw UpstreamError.unauthorized
}
}
private final class NotificationTestClient: SimAdminClientProtocol, @unchecked Sendable {
var routes: [UpstreamRoute] = []
func send(_ route: UpstreamRoute, baseURL: URL, cookie: String?) async throws -> UpstreamResponse {
routes.append(route)
switch route {
case .notificationConfig:
let json = """
{"status":"ok","data":{"channels":[{"type":"webhook","enabled":true},
{"type":"bark","enabled":false}],"rules":[{"enabled":true}]}}
"""
return Self.response(json)
case .notificationLogs(let limit, let offset):
XCTAssertEqual(limit, 50)
XCTAssertEqual(offset, 0)
let json = """
{"status":"ok","data":{"total":3,"logs":[
{"id":"l-1","event_type":"device_offline","status":"success","created_at":"2026-09-08T10:00:00Z"},
{"id":"l-2","event_type":"low_signal","status":"success","created_at":"2026-09-08T10:01:00Z"},
{"id":"l-3","event_type":"device_offline","status":"failed","created_at":"2026-09-08T10:02:00Z"}
]}}
"""
return Self.response(json)
case .notificationQueue(let limit):
XCTAssertEqual(limit, 100)
let json = """
{"status":"ok","data":{"total":2,"items":[
{"id":"q-1","event_type":"device_offline","status":"pending","created_at":"2026-09-08T10:00:00Z"},
{"id":"q-2","event_type":"low_signal","status":"failed","created_at":"2026-09-08T10:01:00Z"}
]}}
"""
return Self.response(json)
case .notificationRetryQueue, .notificationDeleteQueue:
return Self.response(#"{"status":"ok"}"#)
default:
return UpstreamResponse(status: 404, data: Data("{}".utf8))
}
}
private static func response(_ json: String) -> UpstreamResponse {
UpstreamResponse(status: 200, data: Data(json.utf8))
}
}