first commit
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
package remotecontrol
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type fakeSink struct {
|
||||
mu sync.Mutex
|
||||
outputs []string
|
||||
confirms []confirmReq
|
||||
connected bool
|
||||
}
|
||||
|
||||
type confirmReq struct {
|
||||
id, tool, summary string
|
||||
}
|
||||
|
||||
func (f *fakeSink) SendOutput(text string) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.outputs = append(f.outputs, text)
|
||||
}
|
||||
|
||||
func (f *fakeSink) SendConfirm(id, tool, summary string) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.confirms = append(f.confirms, confirmReq{id, tool, summary})
|
||||
}
|
||||
|
||||
func (f *fakeSink) HasClient() bool {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
return f.connected
|
||||
}
|
||||
|
||||
func (f *fakeSink) lastConfirmID() string {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
if len(f.confirms) == 0 {
|
||||
return ""
|
||||
}
|
||||
return f.confirms[len(f.confirms)-1].id
|
||||
}
|
||||
|
||||
func TestBridgeOutputWriterTees(t *testing.T) {
|
||||
sink := &fakeSink{}
|
||||
b := NewBridge(sink)
|
||||
w := b.OutputWriter()
|
||||
n, err := w.Write([]byte("hello world"))
|
||||
if err != nil || n != len("hello world") {
|
||||
t.Fatalf("Write = (%d,%v), want (%d,nil)", n, err, len("hello world"))
|
||||
}
|
||||
sink.mu.Lock()
|
||||
defer sink.mu.Unlock()
|
||||
if len(sink.outputs) != 1 || sink.outputs[0] != "hello world" {
|
||||
t.Fatalf("outputs = %v, want [hello world]", sink.outputs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBridgeRemoteInput(t *testing.T) {
|
||||
b := NewBridge(&fakeSink{})
|
||||
b.OnInput("do a thing")
|
||||
select {
|
||||
case got := <-b.RemoteInput():
|
||||
if got != "do a thing" {
|
||||
t.Fatalf("input = %q, want 'do a thing'", got)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("no input delivered")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBridgeOnInputDropsOldestWhenFull(t *testing.T) {
|
||||
b := NewBridge(&fakeSink{})
|
||||
// Fill beyond capacity; must not block and must retain the newest items.
|
||||
total := remoteInputBuffer + 10
|
||||
for i := range total {
|
||||
b.OnInput(itoa(uint64(i)))
|
||||
}
|
||||
// Drain and ensure we get exactly buffer-size items, ending at the newest.
|
||||
var got []string
|
||||
for {
|
||||
select {
|
||||
case v := <-b.RemoteInput():
|
||||
got = append(got, v)
|
||||
continue
|
||||
default:
|
||||
}
|
||||
break
|
||||
}
|
||||
if len(got) != remoteInputBuffer {
|
||||
t.Fatalf("drained %d items, want %d", len(got), remoteInputBuffer)
|
||||
}
|
||||
if last := got[len(got)-1]; last != itoa(uint64(total-1)) {
|
||||
t.Fatalf("newest = %q, want %q", last, itoa(uint64(total-1)))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBridgeConfirmResolvedRemotely(t *testing.T) {
|
||||
sink := &fakeSink{connected: true}
|
||||
b := NewBridge(sink)
|
||||
|
||||
done := make(chan struct {
|
||||
d Decision
|
||||
remote bool
|
||||
}, 1)
|
||||
go func() {
|
||||
d, remote := b.Confirm(context.Background(), "shell", "rm -rf /tmp/x")
|
||||
done <- struct {
|
||||
d Decision
|
||||
remote bool
|
||||
}{d, remote}
|
||||
}()
|
||||
|
||||
// Wait for the confirm to be sent, then answer it via OnDecide.
|
||||
deadline := time.Now().Add(time.Second)
|
||||
for sink.lastConfirmID() == "" {
|
||||
if time.Now().After(deadline) {
|
||||
t.Fatal("confirm never sent")
|
||||
}
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
}
|
||||
b.OnDecide(sink.lastConfirmID(), true, true)
|
||||
|
||||
select {
|
||||
case r := <-done:
|
||||
if !r.remote {
|
||||
t.Fatal("remote flag = false, want true")
|
||||
}
|
||||
if !r.d.Approve || !r.d.Always {
|
||||
t.Fatalf("decision = %+v, want approve+always", r.d)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("Confirm did not return")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBridgeConfirmCancelledByContext(t *testing.T) {
|
||||
sink := &fakeSink{connected: true}
|
||||
b := NewBridge(sink)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
done := make(chan bool, 1)
|
||||
go func() {
|
||||
_, remote := b.Confirm(ctx, "shell", "ls")
|
||||
done <- remote
|
||||
}()
|
||||
// Let the confirm register, then cancel (simulating a local answer).
|
||||
deadline := time.Now().Add(time.Second)
|
||||
for sink.lastConfirmID() == "" {
|
||||
if time.Now().After(deadline) {
|
||||
t.Fatal("confirm never sent")
|
||||
}
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
}
|
||||
cancel()
|
||||
|
||||
select {
|
||||
case remote := <-done:
|
||||
if remote {
|
||||
t.Fatal("remote flag = true, want false on ctx cancel")
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("Confirm did not return on cancel")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBridgeResolveConfirmUnknown(t *testing.T) {
|
||||
b := NewBridge(&fakeSink{})
|
||||
if b.ResolveConfirm("nope", true, false) {
|
||||
t.Fatal("ResolveConfirm on unknown id = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBridgeEnabled(t *testing.T) {
|
||||
var nilBridge *Bridge
|
||||
if nilBridge.Enabled() {
|
||||
t.Fatal("nil bridge Enabled = true, want false")
|
||||
}
|
||||
sink := &fakeSink{connected: false}
|
||||
b := NewBridge(sink)
|
||||
if b.Enabled() {
|
||||
t.Fatal("Enabled = true with no client, want false")
|
||||
}
|
||||
sink.connected = true
|
||||
if !b.Enabled() {
|
||||
t.Fatal("Enabled = false with client, want true")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user