Files
rtnetmon/internal/monitor/starlink_test.go
T
sneak e3ef4f2f48
check / check (push) Failing after 1s
Add Starlink status lines for the physical gateway pane (closes #3)
When the non-VPN physical gateway is a Starlink dish, two lines are shown
under that pane: state, uptime, obstruction and alert count; then pop-ping
latency and drop rate with downlink/uplink throughput. They turn red when
the dish is not connected or an alert is active.

Detection is a TCP connect to the dish's fixed endpoint 192.168.100.1:9200,
bound to the physical interface the same way the latency probes bind. Until
a dish answers nothing is drawn and no status is fetched, so an absent dish
adds no noise. Status comes from the dish's local get_status gRPC call.

Detection and the fetch sit behind a small Client interface; the loop and
the pure Render function are tested with a fake, no dish and no network.

Model: opus-4-8
2026-09-21 22:51:45 +00:00

142 lines
3.1 KiB
Go

package monitor_test
import (
"context"
"sync"
"testing"
"time"
"git.eeqj.de/sneak/rtnetmon/internal/monitor"
"git.eeqj.de/sneak/rtnetmon/internal/starlink"
)
// fakeDish is a Starlink client for tests: no dish, no network. It records
// how often each method is called and returns programmed results.
type fakeDish struct {
mu sync.Mutex
probeOK bool
status starlink.Status
statusErr error
probeCalls int
statusCalls int
}
func (f *fakeDish) Probe(_ context.Context) bool {
f.mu.Lock()
defer f.mu.Unlock()
f.probeCalls++
return f.probeOK
}
func (f *fakeDish) Status(_ context.Context) (starlink.Status, error) {
f.mu.Lock()
defer f.mu.Unlock()
f.statusCalls++
return f.status, f.statusErr
}
func (f *fakeDish) counts() (int, int) {
f.mu.Lock()
defer f.mu.Unlock()
return f.probeCalls, f.statusCalls
}
func newTwoPaneMonitor() *monitor.Monitor {
return monitor.NewMonitor([]monitor.IfaceSpec{
{Name: ifaceTest0, Label: "A"},
{Name: ifaceTest1, Label: "B"},
}, "")
}
func TestEnableStarlinkSelectsPane(t *testing.T) {
t.Parallel()
mon := newTwoPaneMonitor()
mon.EnableStarlink(ifaceTest1)
if !mon.StarlinkEnabled() {
t.Fatal("StarlinkEnabled = false, want true")
}
if got := mon.StarlinkPaneName(); got != ifaceTest1 {
t.Errorf("StarlinkPaneName = %q, want %q", got, ifaceTest1)
}
}
func TestEnableStarlinkUnknownInterface(t *testing.T) {
t.Parallel()
mon := newTwoPaneMonitor()
mon.EnableStarlink("does-not-exist")
if mon.StarlinkEnabled() {
t.Error("StarlinkEnabled = true for an unknown interface, want false")
}
}
// TestStarlinkNoDishNoStatus verifies that when no dish answers, no status
// is ever fetched and nothing is marked detected.
func TestStarlinkNoDishNoStatus(t *testing.T) {
t.Parallel()
mon := newTwoPaneMonitor()
fake := &fakeDish{probeOK: false}
mon.SetStarlinkClient(ifaceTest1, fake)
ctx := context.Background()
mon.StarlinkProbeStep(ctx)
mon.StarlinkRefreshStep(ctx)
detected, haveStatus, _ := mon.StarlinkSnapshot()
if detected || haveStatus {
t.Errorf("detected=%v haveStatus=%v, want both false", detected, haveStatus)
}
if _, status := fake.counts(); status != 0 {
t.Errorf("status calls = %d, want 0 (no probing noise)", status)
}
}
// TestStarlinkDetectedFetchesStatus verifies that once a dish answers,
// detection triggers a status fetch and later refreshes fetch again.
func TestStarlinkDetectedFetchesStatus(t *testing.T) {
t.Parallel()
mon := newTwoPaneMonitor()
want := starlink.Status{
State: "CONNECTED",
Uptime: 2 * time.Hour,
DownlinkMbps: 100,
}
fake := &fakeDish{probeOK: true, status: want}
mon.SetStarlinkClient(ifaceTest1, fake)
ctx := context.Background()
mon.StarlinkProbeStep(ctx)
detected, haveStatus, got := mon.StarlinkSnapshot()
if !detected || !haveStatus {
t.Fatalf("detected=%v haveStatus=%v, want both true", detected, haveStatus)
}
if got != want {
t.Errorf("status = %+v, want %+v", got, want)
}
mon.StarlinkRefreshStep(ctx)
probe, status := fake.counts()
if probe != 1 {
t.Errorf("probe calls = %d, want 1", probe)
}
if status != 2 {
t.Errorf("status calls = %d, want 2", status)
}
}