mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-24 14:29:07 +02:00
115 lines
3.3 KiB
Go
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")
|
|
}
|