Files
netbird/client/ui/services/daemon_feed_test.go
T
Theodor Midtlien e13dbe8cb4 [client] Improve error surfacing for restricted daemon sockets (#7704)
* Add probe for daemon restriction and improve error surfacing
2026-10-23 11:38:26 +02:00

115 lines
3.3 KiB
Go

//go:build !android && !ios && !freebsd && !js
package services
import (
"context"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
gstatus "google.golang.org/grpc/status"
"github.com/netbirdio/netbird/client/proto"
)
// unreachableDaemon fails every call with Unavailable.
type unreachableDaemon struct {
proto.DaemonServiceClient
}
func (unreachableDaemon) Status(context.Context, *proto.StatusRequest, ...grpc.CallOption) (*proto.StatusResponse, error) {
return nil, gstatus.Error(codes.Unavailable, "connection error")
}
func (unreachableDaemon) SubscribeStatus(context.Context, *proto.StatusRequest, ...grpc.CallOption) (grpc.ServerStreamingClient[proto.StatusResponse], error) {
return nil, gstatus.Error(codes.Unavailable, "connection error")
}
// probingConn is a DaemonConn whose socket probe reports denied.
type probingConn struct {
stubConn
denied bool
}
func (c probingConn) DeniesCaller(context.Context) bool { return c.denied }
type recordingEmitter struct {
mu sync.Mutex
events []Status
got chan struct{}
}
func (e *recordingEmitter) Emit(name string, data ...any) bool {
if name != EventStatusSnapshot {
return true
}
e.mu.Lock()
e.events = append(e.events, data[0].(Status))
e.mu.Unlock()
select {
case e.got <- struct{}{}:
default:
}
return true
}
func TestDaemonFeedGet_TellsDenialFromNotRunning(t *testing.T) {
tests := []struct {
name string
conn DaemonConn
want string
}{
{"socket refuses the user", probingConn{stubConn: stubConn{client: unreachableDaemon{}}, denied: true}, StatusDaemonAccessDenied},
{"daemon not running", probingConn{stubConn: stubConn{client: unreachableDaemon{}}}, StatusDaemonUnavailable},
{"conn cannot probe", stubConn{client: unreachableDaemon{}}, StatusDaemonUnavailable},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
feed := &DaemonFeed{conn: tt.conn}
st, err := feed.Get(context.Background())
require.NoError(t, err)
assert.Equal(t, tt.want, st.Status, "synthetic status for an unreachable daemon")
})
}
}
// The denial is emitted once across stream retries.
func TestDaemonFeedStream_EmitsDenialOnce(t *testing.T) {
emitter := &recordingEmitter{got: make(chan struct{}, 1)}
feed := &DaemonFeed{
conn: probingConn{stubConn: stubConn{client: unreachableDaemon{}}, denied: true},
emitter: emitter,
}
ctx, cancel := context.WithCancel(context.Background())
feed.streamWg.Add(1)
go feed.statusStreamLoop(ctx)
select {
case <-emitter.got:
case <-time.After(5 * time.Second):
t.Fatal("no status emitted")
}
// Let at least one retry run past the first attempt.
time.Sleep(1500 * time.Millisecond)
cancel()
feed.streamWg.Wait()
emitter.mu.Lock()
defer emitter.mu.Unlock()
require.NotEmpty(t, emitter.events)
assert.Len(t, emitter.events, 1, "the outage is reported once across retries")
assert.Equal(t, StatusDaemonAccessDenied, emitter.events[0].Status, "the denial, not not-running")
}
func TestIsDaemonOutage(t *testing.T) {
assert.True(t, IsDaemonOutage(StatusDaemonUnavailable), "not running is an outage")
assert.True(t, IsDaemonOutage(StatusDaemonAccessDenied), "a refused user is an outage")
assert.False(t, IsDaemonOutage(StatusConnected), "a live daemon is not")
}