visionA/visionA-backend/internal/device/inmemory_repository_test.go
jim800121chen 59c57fa481 feat(device): WP-B repository 接 agents 模型 + exchange 重塑(A' 走向第二階段 Go 層)
- Device struct 加 4 欄(agent_id/agent_local_device_id/registered_at/
  is_representative)+ deviceColumns 13→17 + scanDevice/SaveTx 讀寫新欄
- 新增 internal/agent package(domain + interface + in-memory + PG repo):
  GetOrCreateAgentTx/GetByOwnerTx,advisory lock 序列化同 owner get-or-create
- exchange 重塑:建/復用 agent → representative device(綁 session_tokens、
  serial=NULL)→ loop 建 N 顆真 USB device(R1 完整 N 顆非只第一顆)
- List filter is_representative=false + DeviceListItem 回傳 agent_id/registered_at
- 併入 WP-0/0005 follow-up Minor:Mi#2 lost-update 收斂(tx 內查詢+局部更新)
  / Mi#3 過時註解 / Mi#4 空 serial 回 ErrNotFound / Mi#5 serial 白名單
  ^0x[0-9A-Fa-f]{8}$ + 去重 / S-1 firmware forward-compat / S-2 device Name 衍生

守 ADR-018 A'(一 owner N agents、session_tokens FK 物理不動、不加 owner
unique 為多機器留路)。Reviewer 通過(0C/0M)。5 套件 dbtest 130 全綠
(db 19/device 37/agent 13/api 172/cmd 60)、build/vet/test 綠。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 11:40:17 +08:00

177 lines
5.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package device
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestInMemoryRepository_SaveAndGet(t *testing.T) {
ctx := context.Background()
r := NewInMemoryRepository()
d := &Device{
ID: "dev-1",
OwnerUserID: "user-1",
Name: "Lab KL520",
DeviceType: "kl520",
SerialNumber: "KL520-AAA",
RemoteStatus: RemoteStatusOffline,
Status: USBStatusUnknown,
}
require.NoError(t, r.Save(ctx, d))
got, err := r.Get(ctx, "dev-1")
require.NoError(t, err)
assert.Equal(t, "Lab KL520", got.Name)
assert.False(t, got.CreatedAt.IsZero())
assert.False(t, got.UpdatedAt.IsZero())
}
func TestInMemoryRepository_Get_NotFound(t *testing.T) {
r := NewInMemoryRepository()
_, err := r.Get(context.Background(), "nope")
assert.ErrorIs(t, err, ErrNotFound)
}
func TestInMemoryRepository_Save_RequiresID(t *testing.T) {
r := NewInMemoryRepository()
err := r.Save(context.Background(), &Device{Name: "no-id"})
assert.Error(t, err)
}
func TestInMemoryRepository_GetBySerial(t *testing.T) {
ctx := context.Background()
r := NewInMemoryRepository()
require.NoError(t, r.Save(ctx, &Device{
ID: "dev-1", OwnerUserID: "user-A", SerialNumber: "S-1",
}))
require.NoError(t, r.Save(ctx, &Device{
ID: "dev-2", OwnerUserID: "user-B", SerialNumber: "S-1",
}))
got, err := r.GetBySerial(ctx, "user-A", "S-1")
require.NoError(t, err)
assert.Equal(t, "dev-1", got.ID)
_, err = r.GetBySerial(ctx, "user-C", "S-1")
assert.ErrorIs(t, err, ErrNotFound)
}
func TestInMemoryRepository_List_ByOwner(t *testing.T) {
ctx := context.Background()
r := NewInMemoryRepository()
require.NoError(t, r.Save(ctx, &Device{ID: "a", OwnerUserID: "u1"}))
require.NoError(t, r.Save(ctx, &Device{ID: "b", OwnerUserID: "u1"}))
require.NoError(t, r.Save(ctx, &Device{ID: "c", OwnerUserID: "u2"}))
listU1, err := r.List(ctx, "u1")
require.NoError(t, err)
assert.Len(t, listU1, 2)
listU3, err := r.List(ctx, "u3")
require.NoError(t, err)
assert.Empty(t, listU3)
}
func TestInMemoryRepository_Delete_SoftDelete(t *testing.T) {
ctx := context.Background()
r := NewInMemoryRepository()
require.NoError(t, r.Save(ctx, &Device{ID: "dev-1", OwnerUserID: "u"}))
require.NoError(t, r.Delete(ctx, "dev-1"))
// Get 應該找不到
_, err := r.Get(ctx, "dev-1")
assert.ErrorIs(t, err, ErrNotFound)
// List 也不該列出
list, _ := r.List(ctx, "u")
assert.Empty(t, list)
// 再次 Delete 應回 ErrNotFound已軟刪除
assert.ErrorIs(t, r.Delete(ctx, "dev-1"), ErrNotFound)
}
func TestInMemoryRepository_Save_PreservesCreatedAt(t *testing.T) {
ctx := context.Background()
r := NewInMemoryRepository()
require.NoError(t, r.Save(ctx, &Device{ID: "dev-1", OwnerUserID: "u"}))
first, err := r.Get(ctx, "dev-1")
require.NoError(t, err)
createdAt := first.CreatedAt
// 更新(應保留 CreatedAt
updated := *first
updated.Name = "Updated"
require.NoError(t, r.Save(ctx, &updated))
got, err := r.Get(ctx, "dev-1")
require.NoError(t, err)
assert.Equal(t, "Updated", got.Name)
assert.Equal(t, createdAt, got.CreatedAt, "CreatedAt 應保留原值")
assert.True(t, got.UpdatedAt.After(createdAt) || got.UpdatedAt.Equal(createdAt))
}
// ---------------------------------------------------------------------------
// B4List filter representative + Mi#4 空 serial + representative 查詢)
// ---------------------------------------------------------------------------
// List filterrepresentative device 不出現在清單(對齊 PG 版)。
func TestInMemoryRepository_List_FiltersRepresentative(t *testing.T) {
ctx := context.Background()
r := NewInMemoryRepository()
owner := "owner-1"
require.NoError(t, r.Save(ctx, &Device{ID: "rep", OwnerUserID: owner, Name: "rep", AgentID: "ag", IsRepresentative: true}))
require.NoError(t, r.Save(ctx, &Device{ID: "usb1", OwnerUserID: owner, Name: "usb1", SerialNumber: "0x11111111", AgentID: "ag"}))
require.NoError(t, r.Save(ctx, &Device{ID: "usb2", OwnerUserID: owner, Name: "usb2", SerialNumber: "0x22222222", AgentID: "ag"}))
list, err := r.List(ctx, owner)
require.NoError(t, err)
require.Len(t, list, 2, "List 只回真 USB排除 representative")
for _, d := range list {
assert.False(t, d.IsRepresentative)
}
}
// Mi#4in-memory GetBySerial 空 serial → ErrNotFound對齊 PG GetBySerialTx
func TestInMemoryRepository_GetBySerial_EmptyReturnsNotFound(t *testing.T) {
ctx := context.Background()
r := NewInMemoryRepository()
owner := "owner-1"
require.NoError(t, r.Save(ctx, &Device{ID: "null1", OwnerUserID: owner, Name: "null1"})) // serial 空
_, err := r.GetBySerial(ctx, owner, "")
assert.ErrorIs(t, err, ErrNotFound, "空 serial 查詢應回 ErrNotFound")
}
// GetRepresentativeByAgentTx找該 agent 的 representative device。
func TestInMemoryRepository_GetRepresentativeByAgent(t *testing.T) {
ctx := context.Background()
r := NewInMemoryRepository()
owner := "owner-1"
// 無 representative 時回 ErrNotFound。
_, err := r.GetRepresentativeByAgentTx(ctx, nil, "ag")
assert.ErrorIs(t, err, ErrNotFound)
require.NoError(t, r.Save(ctx, &Device{ID: "rep", OwnerUserID: owner, Name: "rep", AgentID: "ag", IsRepresentative: true}))
require.NoError(t, r.Save(ctx, &Device{ID: "usb", OwnerUserID: owner, Name: "usb", SerialNumber: "0x11111111", AgentID: "ag"}))
got, err := r.GetRepresentativeByAgentTx(ctx, nil, "ag")
require.NoError(t, err)
assert.Equal(t, "rep", got.ID, "應回 representative device非真 USB")
assert.True(t, got.IsRepresentative)
// 不同 agent 查不到。
_, err = r.GetRepresentativeByAgentTx(ctx, nil, "other-agent")
assert.ErrorIs(t, err, ErrNotFound)
}