Files
Cyrene_v2/internal/iot/iot_test.go
T
AskaEth 2593765add docs(iot): 存量英文注释中文化(仅注释,代码零字节改动)
- 5 个文件:123 行英文注释 → 99 行中文注释(中文更紧凑,内容无删减)。
- 机械校验(git diff -U0 去掉 // 行与空行)输出为空;gofmt -l 干净(改动前亦干净)。
- 验收:go build/vet/test ./internal/iot/... 全绿。
2026-10-04 16:58:00 +08:00

298 lines
9.2 KiB
Go

package iot
import (
"context"
"errors"
"testing"
"time"
"git.yeij.top/AskaEth/Cyrene_v2/internal/bus"
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
)
// fakeProvider 是桩 Provider,用于验证 Manager.Register 与设备操作路由,
// 而不触碰具体的 mockProvider。
type fakeProvider struct {
name string
device Device
ctrl func(*Device)
}
func (f fakeProvider) Name() string { return f.name }
func (f fakeProvider) List(context.Context) ([]Device, error) {
return []Device{f.device}, nil
}
func (f fakeProvider) Control(_ context.Context, id, _ string, _ map[string]any) (*Device, error) {
if id != f.device.ID {
return nil, ErrDeviceNotFound
}
if f.ctrl != nil {
f.ctrl(&f.device)
}
cp := f.device.Clone()
return &cp, nil
}
func (f fakeProvider) Status(_ context.Context, id string) (*Device, error) {
if id != f.device.ID {
return nil, ErrDeviceNotFound
}
cp := f.device.Clone()
return &cp, nil
}
// recvIoT 阻塞等待,直到收到一条 DeviceUpdate 或超过等待时限。
func recvIoT(t *testing.T, ch <-chan DeviceUpdate, d time.Duration) DeviceUpdate {
t.Helper()
select {
case v := <-ch:
return v
case <-time.After(d):
t.Fatalf("timed out waiting for a DeviceUpdate after %v", d)
return DeviceUpdate{}
}
}
func TestNew_ProviderSelection(t *testing.T) {
t.Parallel()
tests := []struct {
name string
provider string
wantLen int
wantErr error
}{
{name: "mock default", provider: "", wantLen: 8},
{name: "mock explicit", provider: "mock", wantLen: 8},
{name: "none disabled", provider: "none", wantLen: 0},
{name: "unknown", provider: "bogus", wantErr: ErrUnknownProvider},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
m, err := New(config.IoTConfig{Provider: tt.provider}, nil)
if tt.wantErr != nil {
if !errors.Is(err, tt.wantErr) {
t.Fatalf("New error = %v, want %v", err, tt.wantErr)
}
return
}
if err != nil {
t.Fatalf("New: %v", err)
}
devices, err := m.List(context.Background())
if err != nil {
t.Fatalf("List: %v", err)
}
if len(devices) != tt.wantLen {
t.Fatalf("List returned %d devices, want %d", len(devices), tt.wantLen)
}
})
}
}
func TestList_MockCoversAllTypes(t *testing.T) {
t.Parallel()
m, err := New(config.IoTConfig{Provider: "mock"}, nil)
if err != nil {
t.Fatalf("New: %v", err)
}
devices, err := m.List(context.Background())
if err != nil {
t.Fatalf("List: %v", err)
}
if len(devices) != 8 {
t.Fatalf("List returned %d devices, want 8", len(devices))
}
seenID := map[string]bool{}
seenType := map[string]int{}
var anyOnline, anyOffline bool
for _, d := range devices {
if seenID[d.ID] {
t.Fatalf("duplicate device ID %q", d.ID)
}
seenID[d.ID] = true
seenType[d.Type]++
if d.Online {
anyOnline = true
} else {
anyOffline = true
}
}
for _, want := range []string{"light", "ac", "curtain", "sensor", "lock"} {
if seenType[want] == 0 {
t.Fatalf("List has no device of type %q (types: %v)", want, seenType)
}
}
if !anyOnline || !anyOffline {
t.Fatalf("List should contain both online and offline devices (online=%v offline=%v)", anyOnline, anyOffline)
}
}
func TestStatus_ExistingAndMissing(t *testing.T) {
t.Parallel()
m, err := New(config.IoTConfig{Provider: "mock"}, nil)
if err != nil {
t.Fatalf("New: %v", err)
}
d, err := m.Status(context.Background(), "ac.bedroom")
if err != nil {
t.Fatalf("Status: %v", err)
}
if d.Online {
t.Fatalf("ac.bedroom should be offline, got online=%v", d.Online)
}
if _, err := m.Status(context.Background(), "nope"); !errors.Is(err, ErrDeviceNotFound) {
t.Fatalf("Status(missing) error = %v, want ErrDeviceNotFound", err)
}
}
func TestControl_Actions(t *testing.T) {
t.Parallel()
tests := []struct {
name string
deviceID string
action string
params map[string]any
wantKey string
wantVal any
}{
{name: "light on", deviceID: "light.living", action: "on", wantKey: "on", wantVal: true},
{name: "light off", deviceID: "light.living", action: "off", wantKey: "on", wantVal: false},
{name: "light toggle", deviceID: "light.living", action: "toggle", wantKey: "on", wantVal: true},
{name: "light brightness", deviceID: "light.living", action: "set_brightness", params: map[string]any{"brightness": 80}, wantKey: "brightness", wantVal: 80},
{name: "ac temperature", deviceID: "ac.living", action: "set_temperature", params: map[string]any{"temperature": 24}, wantKey: "temperature", wantVal: 24},
{name: "ac mode", deviceID: "ac.living", action: "set_mode", params: map[string]any{"mode": "heat"}, wantKey: "mode", wantVal: "heat"},
{name: "curtain open", deviceID: "curtain.living", action: "open", wantKey: "position", wantVal: 100},
{name: "curtain close", deviceID: "curtain.living", action: "close", wantKey: "position", wantVal: 0},
{name: "lock lock", deviceID: "lock.front", action: "lock", wantKey: "locked", wantVal: true},
{name: "lock unlock", deviceID: "lock.front", action: "unlock", wantKey: "locked", wantVal: false},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
m, err := New(config.IoTConfig{Provider: "mock"}, nil)
if err != nil {
t.Fatalf("New: %v", err)
}
d, err := m.Control(context.Background(), tt.deviceID, tt.action, tt.params)
if err != nil {
t.Fatalf("Control: %v", err)
}
if got := d.State[tt.wantKey]; got != tt.wantVal {
t.Fatalf("state[%s] = %v, want %v", tt.wantKey, got, tt.wantVal)
}
})
}
}
func TestControl_CapabilityConstraints(t *testing.T) {
t.Parallel()
tests := []struct {
name string
deviceID string
action string
params map[string]any
wantErr error
}{
{name: "sensor read-only", deviceID: "sensor.env", action: "read", wantErr: ErrUnsupported},
{name: "sensor any action", deviceID: "sensor.env", action: "on", wantErr: ErrUnsupported},
{name: "unknown action", deviceID: "light.living", action: "explode", wantErr: ErrUnsupported},
{name: "missing device", deviceID: "nope", action: "on", wantErr: ErrDeviceNotFound},
{name: "offline device", deviceID: "ac.bedroom", action: "on", wantErr: ErrOffline},
{name: "non-numeric brightness", deviceID: "light.living", action: "set_brightness", params: map[string]any{"brightness": "x"}, wantErr: ErrInvalidParam},
{name: "missing brightness param", deviceID: "light.living", action: "set_brightness", wantErr: ErrInvalidParam},
{name: "ac invalid mode", deviceID: "ac.living", action: "set_mode", params: map[string]any{"mode": "freeze"}, wantErr: ErrUnsupported},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
m, err := New(config.IoTConfig{Provider: "mock"}, nil)
if err != nil {
t.Fatalf("New: %v", err)
}
if _, err := m.Control(context.Background(), tt.deviceID, tt.action, tt.params); !errors.Is(err, tt.wantErr) {
t.Fatalf("Control error = %v, want %v", err, tt.wantErr)
}
})
}
}
func TestControl_PublishesDeviceUpdate(t *testing.T) {
t.Parallel()
m, err := New(config.IoTConfig{Provider: "mock"}, nil)
if err != nil {
t.Fatalf("New: %v", err)
}
updates := make(chan DeviceUpdate, 4)
unsub, err := m.Bus().Subscribe(context.Background(), DeviceUpdateSubject, func(_ context.Context, e bus.Event) error {
updates <- e.Payload.(DeviceUpdate)
return nil
})
if err != nil {
t.Fatalf("Subscribe: %v", err)
}
defer unsub()
if _, err := m.Control(context.Background(), "light.living", "on", nil); err != nil {
t.Fatalf("Control: %v", err)
}
u := recvIoT(t, updates, 2*time.Second)
if u.Provider != "mock" {
t.Fatalf("event provider = %q, want mock", u.Provider)
}
if u.Action != "on" {
t.Fatalf("event action = %q, want on", u.Action)
}
if u.Device.ID != "light.living" {
t.Fatalf("event device ID = %q, want light.living", u.Device.ID)
}
if got := u.Device.State["on"]; got != true {
t.Fatalf("event state on = %v, want true", got)
}
}
func TestRegister_ProviderSource(t *testing.T) {
t.Parallel()
m, err := New(config.IoTConfig{Provider: "none"}, nil)
if err != nil {
t.Fatalf("New: %v", err)
}
dev := Device{ID: "fake.dev", Name: "fake", Type: "light", Online: true, State: map[string]any{"on": false}}
if err := m.Register(fakeProvider{name: "fake", device: dev}); err != nil {
t.Fatalf("Register: %v", err)
}
devices, err := m.List(context.Background())
if err != nil {
t.Fatalf("List: %v", err)
}
if len(devices) != 1 || devices[0].ID != "fake.dev" {
t.Fatalf("List = %+v, want one fake.dev device", devices)
}
if err := m.Register(fakeProvider{name: "fake"}); !errors.Is(err, ErrDuplicate) {
t.Fatalf("duplicate Register error = %v, want ErrDuplicate", err)
}
if err := m.Register(nil); !errors.Is(err, ErrInvalidArgument) {
t.Fatalf("nil Register error = %v, want ErrInvalidArgument", err)
}
}
func TestNone_Disabled(t *testing.T) {
t.Parallel()
m, err := New(config.IoTConfig{Provider: "none"}, nil)
if err != nil {
t.Fatalf("New: %v", err)
}
devices, err := m.List(context.Background())
if err != nil {
t.Fatalf("List: %v", err)
}
if len(devices) != 0 {
t.Fatalf("List returned %d devices, want 0", len(devices))
}
if _, err := m.Control(context.Background(), "x", "on", nil); !errors.Is(err, ErrProviderNotRegistered) {
t.Fatalf("Control error = %v, want ErrProviderNotRegistered", err)
}
}