164 lines
6.4 KiB
Swift
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))
|
|
}
|
|
}
|