package device import ( "context" "testing" "time" "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)) } // --------------------------------------------------------------------------- // B4(List filter representative + Mi#4 空 serial + representative 查詢) // --------------------------------------------------------------------------- // List filter:representative 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#4:in-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) } // --------------------------------------------------------------------------- // SetRegistered(註冊軸單欄翻轉,feature-device-mgmt-tdd §3.3) // --------------------------------------------------------------------------- // SetRegistered set → registered_at 有值;set nil → 清空。 func TestInMemoryRepository_SetRegistered_SetAndClear(t *testing.T) { ctx := context.Background() r := NewInMemoryRepository() require.NoError(t, r.Save(ctx, &Device{ID: "d1", OwnerUserID: "u", SerialNumber: "S-1"})) // 初始未註冊。 got, err := r.Get(ctx, "d1") require.NoError(t, err) require.Nil(t, got.RegisteredAt) // set → 已註冊。 now := time.Now().UTC() require.NoError(t, r.SetRegistered(ctx, "d1", &now)) got, err = r.Get(ctx, "d1") require.NoError(t, err) require.NotNil(t, got.RegisteredAt) assert.True(t, now.Equal(*got.RegisteredAt)) // set nil → 退回未註冊,列仍在。 require.NoError(t, r.SetRegistered(ctx, "d1", nil)) got, err = r.Get(ctx, "d1") require.NoError(t, err, "unregister 不刪列") assert.Nil(t, got.RegisteredAt) } // SetRegistered 冪等:對已 NULL 的列再 set nil → 成功(no-op)。 func TestInMemoryRepository_SetRegistered_ClearIdempotent(t *testing.T) { ctx := context.Background() r := NewInMemoryRepository() require.NoError(t, r.Save(ctx, &Device{ID: "d1", OwnerUserID: "u", SerialNumber: "S-1"})) require.NoError(t, r.SetRegistered(ctx, "d1", nil), "未註冊清 nil 應冪等成功") got, _ := r.Get(ctx, "d1") assert.Nil(t, got.RegisteredAt) } // SetRegistered 對 representative device → ErrNotFound(縱深第三層)。 func TestInMemoryRepository_SetRegistered_RejectsRepresentative(t *testing.T) { ctx := context.Background() r := NewInMemoryRepository() require.NoError(t, r.Save(ctx, &Device{ID: "rep", OwnerUserID: "u", AgentID: "ag", IsRepresentative: true})) now := time.Now().UTC() assert.ErrorIs(t, r.SetRegistered(ctx, "rep", &now), ErrNotFound, "representative 不可註冊") } // SetRegistered 對已軟刪 device → ErrNotFound。 func TestInMemoryRepository_SetRegistered_RejectsDeleted(t *testing.T) { ctx := context.Background() r := NewInMemoryRepository() require.NoError(t, r.Save(ctx, &Device{ID: "d1", OwnerUserID: "u", SerialNumber: "S-1"})) require.NoError(t, r.Delete(ctx, "d1")) now := time.Now().UTC() assert.ErrorIs(t, r.SetRegistered(ctx, "d1", &now), ErrNotFound) } // SetRegistered 對不存在 device → ErrNotFound。 func TestInMemoryRepository_SetRegistered_NotFound(t *testing.T) { r := NewInMemoryRepository() now := time.Now().UTC() assert.ErrorIs(t, r.SetRegistered(context.Background(), "ghost", &now), ErrNotFound) }