147 lines
5.3 KiB
Go
147 lines
5.3 KiB
Go
package iot
|
||
|
||
import (
|
||
"context"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/bus"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/urgency"
|
||
)
|
||
|
||
// captureUpdates 订阅 DeviceUpdateSubject 并把收到的载荷送进返回的通道。
|
||
func captureUpdates(t *testing.T, m *Manager) <-chan DeviceUpdate {
|
||
t.Helper()
|
||
ch := make(chan DeviceUpdate, 8)
|
||
unsub, err := m.Bus().Subscribe(context.Background(), DeviceUpdateSubject, func(_ context.Context, e bus.Event) error {
|
||
if d, ok := e.Payload.(DeviceUpdate); ok {
|
||
ch <- d
|
||
}
|
||
return nil
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("Subscribe: %v", err)
|
||
}
|
||
t.Cleanup(unsub)
|
||
return ch
|
||
}
|
||
|
||
// TestControl_UnlockIsCriticalSafetyEvent 验证门锁开启被标成安全域:
|
||
// 这是 IoT 侧唯一的严重度升级出处,也是 quiet hours 豁免的生产端。
|
||
func TestControl_UnlockIsCriticalSafetyEvent(t *testing.T) {
|
||
t.Parallel()
|
||
m, err := New(config.IoTConfig{Provider: "mock"}, nil)
|
||
if err != nil {
|
||
t.Fatalf("New: %v", err)
|
||
}
|
||
updates := captureUpdates(t, m)
|
||
|
||
if _, err := m.Control(context.Background(), "lock.front", "unlock", nil); err != nil {
|
||
t.Fatalf("Control: %v", err)
|
||
}
|
||
got := recvIoT(t, updates, 2*time.Second)
|
||
if got.Severity != SeverityCritical {
|
||
t.Fatalf("Severity = %v, want %v (an unlocked door is a safety event)", got.Severity, SeverityCritical)
|
||
}
|
||
if !strings.Contains(got.Message, "安全域") {
|
||
t.Fatalf("Message = %q, want it to name the safety domain", got.Message)
|
||
}
|
||
}
|
||
|
||
// TestControl_RoutineStaysRoutine 是保守默认的反面断言:常规设备变化(以及
|
||
// 锁门这类非安全域动作)绝不被升级——安静时段照旧扣下它们。
|
||
func TestControl_RoutineStaysRoutine(t *testing.T) {
|
||
t.Parallel()
|
||
cases := []struct {
|
||
name string
|
||
device string
|
||
action string
|
||
}{
|
||
{name: "开灯", device: "light.living", action: "on"},
|
||
{name: "空调设定温度", device: "ac.living", action: "set_temperature"},
|
||
{name: "锁门", device: "lock.front", action: "lock"},
|
||
}
|
||
for _, tc := range cases {
|
||
tc := tc
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
t.Parallel()
|
||
m, err := New(config.IoTConfig{Provider: "mock"}, nil)
|
||
if err != nil {
|
||
t.Fatalf("New: %v", err)
|
||
}
|
||
updates := captureUpdates(t, m)
|
||
params := map[string]any{"temperature": 25}
|
||
if _, err := m.Control(context.Background(), tc.device, tc.action, params); err != nil {
|
||
t.Fatalf("Control: %v", err)
|
||
}
|
||
got := recvIoT(t, updates, 2*time.Second)
|
||
if got.Severity != SeverityRoutine {
|
||
t.Fatalf("Severity = %v, want %v (routine transitions must stay routine)", got.Severity, SeverityRoutine)
|
||
}
|
||
if strings.Contains(got.Message, "安全域") {
|
||
t.Fatalf("Message = %q, want no safety-domain wording", got.Message)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestAlertSeverity_String 验证严重度标签经通用词汇转发(日志与记录依赖它),
|
||
// 且 IoT 侧的类型就是通用紧急度的别名 —— 它没有自己的专有严重度体系。
|
||
func TestAlertSeverity_String(t *testing.T) {
|
||
t.Parallel()
|
||
cases := []AlertSeverity{SeverityRoutine, SeverityWarning, SeverityCritical, AlertSeverity(7)}
|
||
for _, in := range cases {
|
||
if got, want := in.String(), urgency.Level(int(in)).String(); got != want {
|
||
t.Fatalf("AlertSeverity(%d).String() = %q, want the generic label %q", int(in), got, want)
|
||
}
|
||
}
|
||
if got := SeverityCritical.String(); got != "critical" {
|
||
t.Fatalf("SeverityCritical.String() = %q, want %q", got, "critical")
|
||
}
|
||
}
|
||
|
||
// TestDeviceUpdate_DeclaresUrgencyOnGenericChannel 钉住 IoT 走的是**通用**通道:
|
||
// 设备跃迁载荷实现 urgency.Declarer,backthink 因此不必认识 IoT 的专有字段;
|
||
// 未标注严重度的跃迁声明零值 routine(默认保守)。
|
||
func TestDeviceUpdate_DeclaresUrgencyOnGenericChannel(t *testing.T) {
|
||
t.Parallel()
|
||
var declared urgency.Declarer = DeviceUpdate{Severity: SeverityCritical}
|
||
if got := declared.Urgency(); got != urgency.LevelCritical {
|
||
t.Fatalf("DeviceUpdate.Urgency() = %v, want %v", got, urgency.LevelCritical)
|
||
}
|
||
if got := (DeviceUpdate{}).Urgency(); got != urgency.LevelRoutine {
|
||
t.Fatalf("unlabelled DeviceUpdate.Urgency() = %v, want %v", got, urgency.LevelRoutine)
|
||
}
|
||
}
|
||
|
||
// TestSafetySeverity_RuleTable 直接钉住规则表:未登记的设备类型与动作一律不升级。
|
||
func TestSafetySeverity_RuleTable(t *testing.T) {
|
||
t.Parallel()
|
||
cases := []struct {
|
||
name string
|
||
deviceType string
|
||
action string
|
||
wantSev AlertSeverity
|
||
wantHit bool
|
||
}{
|
||
{name: "门锁开启_命中", deviceType: "lock", action: "unlock", wantSev: SeverityCritical, wantHit: true},
|
||
{name: "门锁锁闭_不命中", deviceType: "lock", action: "lock", wantSev: SeverityRoutine, wantHit: false},
|
||
{name: "门锁翻转_不命中", deviceType: "lock", action: "toggle", wantSev: SeverityRoutine, wantHit: false},
|
||
{name: "灯_不命中", deviceType: "light", action: "on", wantSev: SeverityRoutine, wantHit: false},
|
||
{name: "传感器_不命中", deviceType: "sensor", action: "unlock", wantSev: SeverityRoutine, wantHit: false},
|
||
}
|
||
for _, tc := range cases {
|
||
tc := tc
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
t.Parallel()
|
||
sev, hit := safetySeverity(tc.deviceType, tc.action)
|
||
if hit != tc.wantHit || sev != tc.wantSev {
|
||
t.Fatalf("safetySeverity(%q, %q) = (%v, %v), want (%v, %v)",
|
||
tc.deviceType, tc.action, sev, hit, tc.wantSev, tc.wantHit)
|
||
}
|
||
})
|
||
}
|
||
}
|