Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 159 additions & 0 deletions zz_adapters_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// SPDX-License-Identifier: AGPL-3.0-or-later

package runtime_test

import (
"testing"
"time"

"github.com/TeoSlayer/pilotprotocol/pkg/daemon"
"github.com/pilot-protocol/handshake"
"github.com/pilot-protocol/runtime"
)

// TestHandshakeRuntime_Accessors exercises the read-only accessors on a
// HandshakeRuntime backed by a freshly-constructed daemon. The daemon has
// no identity, no handshake service, no registry client, so every accessor
// should return zero / nil / sensible-fallback.
func TestHandshakeRuntime_Accessors(t *testing.T) {
t.Parallel()
d := daemon.New(daemon.Config{})
hr := runtime.NewHandshakeRuntime(d)

if got := hr.NodeID(); got != 0 {
t.Errorf("NodeID = %d, want 0", got)
}
if hr.HasIdentity() {
t.Errorf("HasIdentity = true, want false")
}
if pk := hr.PublicKey(); pk != nil {
t.Errorf("PublicKey = %v, want nil", pk)
}
if sig := hr.Sign([]byte("hello")); sig != nil {
t.Errorf("Sign on nil identity = %v, want nil", sig)
}
if got := hr.IdentityPath(); got != "" {
t.Errorf("IdentityPath = %q, want empty", got)
}
if hr.TrustAutoApprove() {
t.Errorf("TrustAutoApprove = true, want false (zero Config)")
}
if name, ok := hr.IsTrusted(0xCAFE); ok || name != "" {
t.Errorf("IsTrusted(0xCAFE) = (%q, %v); want (\"\", false)", name, ok)
}
// PublishEvent on a daemon with a bus should not panic.
hr.PublishEvent("test.topic", map[string]any{"k": "v"})

// RemoveTunnelPeer on a daemon with an empty tunnel manager is a no-op.
hr.RemoveTunnelPeer(0xDEAD)

// Registry on a daemon with nil regConn returns nil.
if reg := hr.Registry(); reg != nil {
t.Errorf("Registry = %v, want nil (no regConn)", reg)
}
}

// TestHandshakeRuntime_PortListener exercises the Listener-bind path.
func TestHandshakeRuntime_PortListener(t *testing.T) {
t.Parallel()
d := daemon.New(daemon.Config{})
hr := runtime.NewHandshakeRuntime(d)

ln, err := hr.PortListener(54321)
if err != nil {
t.Fatalf("PortListener: %v", err)
}
if ln == nil {
t.Fatal("listener is nil")
}
if err := ln.Close(); err != nil {
t.Errorf("Close: %v", err)
}
}

// TestPolicyRuntime_Accessors exercises read-only PolicyRuntime methods.
func TestPolicyRuntime_Accessors(t *testing.T) {
t.Parallel()
d := daemon.New(daemon.Config{})
pr := runtime.NewPolicyRuntime(d)

if got := pr.NodeID(); got != 0 {
t.Errorf("NodeID = %d, want 0", got)
}
if got := pr.AdminToken(); got != "" {
t.Errorf("AdminToken = %q, want empty", got)
}
if peers := pr.TrustedPeers(); len(peers) != 0 {
t.Errorf("TrustedPeers = %v, want empty (no handshakes)", peers)
}
pr.PublishEvent("policy.test", map[string]any{"x": 1})
pr.SetMemberTags(1, []string{"a", "b"})

// HandshakeRevokeTrust / HandshakeSendRequest both error when
// handshakes == nil (the documented contract).
if err := pr.RevokeTrust(0xCAFE); err == nil {
t.Error("RevokeTrust on nil handshakes: want error, got nil")
}
if err := pr.SendHandshakeRequest(0xCAFE, "why"); err == nil {
t.Error("SendHandshakeRequest on nil handshakes: want error, got nil")
}
}

// TestNewHandshakeServiceAdapter wires a real handshake.Service to its
// daemon.HandshakeService adapter and exercises the read-only methods.
// The service has an empty manager — all trust queries return zero state.
func TestNewHandshakeServiceAdapter(t *testing.T) {
t.Parallel()
d := daemon.New(daemon.Config{})
hr := runtime.NewHandshakeRuntime(d)
svc := handshake.NewService(hr)
defer svc.Manager().Stop()

adapter := runtime.NewHandshakeServiceAdapter(svc)
if adapter == nil {
t.Fatal("adapter is nil")
}

// All read-only accessors should return zero / empty / false on a
// fresh manager.
if adapter.IsTrusted(0xCAFE) {
t.Errorf("IsTrusted on fresh manager: want false")
}
if got := adapter.TrustedPeers(); len(got) != 0 {
t.Errorf("TrustedPeers = %v, want empty", got)
}
if got := adapter.PendingRequests(); len(got) != 0 {
t.Errorf("PendingRequests = %v, want empty", got)
}
if got := adapter.PendingCount(); got != 0 {
t.Errorf("PendingCount = %d, want 0", got)
}
// WaitForTrust returns false on timeout (peer never approves).
if adapter.WaitForTrust(0xCAFE, 10*time.Millisecond) {
t.Errorf("WaitForTrust: want false (timeout)")
}

// ProcessRelayedRequest stages a pending request — exercises the
// internal map-write path without going through the network.
adapter.ProcessRelayedRequest(0x1234, "test")
if got := adapter.PendingCount(); got != 1 {
t.Errorf("PendingCount after relayed request: %d, want 1", got)
}
if got := adapter.PendingRequests(); len(got) != 1 {
t.Errorf("PendingRequests after relayed request: %d, want 1", len(got))
}
adapter.ProcessRelayedApproval(0x5678) // unknown peer — should be safe no-op
adapter.ProcessRelayedRejection(0x5678)

// Stop is idempotent.
adapter.Stop()
adapter.Stop()
}

// TestAsDaemonPolicyManager_Nil exercises the nil short-circuit branch.
func TestAsDaemonPolicyManager_Nil(t *testing.T) {
t.Parallel()
if got := runtime.AsDaemonPolicyManager(nil); got != nil {
t.Errorf("AsDaemonPolicyManager(nil) = %v, want nil", got)
}
}
176 changes: 176 additions & 0 deletions zz_deps_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// SPDX-License-Identifier: AGPL-3.0-or-later

package runtime_test

import (
"context"
"errors"
"testing"
"time"

"github.com/TeoSlayer/pilotprotocol/pkg/coreapi"
"github.com/TeoSlayer/pilotprotocol/pkg/daemon"
"github.com/pilot-protocol/runtime"
)

// fakeService is a Service that captures the Deps it receives in Start
// so tests can exercise the adapter implementations runtime.New wires.
type fakeService struct {
name string
order int
started bool
stopped bool
gotDeps coreapi.Deps
startErr error
stopErr error
}

func (s *fakeService) Name() string { return s.name }
func (s *fakeService) Order() int { return s.order }
func (s *fakeService) Start(_ context.Context, deps coreapi.Deps) error {
s.gotDeps = deps
s.started = true
return s.startErr
}
func (s *fakeService) Stop(_ context.Context) error {
s.stopped = true
return s.stopErr
}

// TestRuntime_StartPlugins_CapturesDeps confirms the Runtime wires
// daemonStreams / daemonIdentity / daemonEventBus into Deps for plugins.
func TestRuntime_StartPlugins_CapturesDeps(t *testing.T) {
t.Parallel()
d := daemon.New(daemon.Config{})
rt := runtime.New(d)

svc := &fakeService{name: "fake", order: 100}
if err := rt.Register(svc); err != nil {
t.Fatalf("Register: %v", err)
}

ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()

if err := rt.StartPlugins(ctx); err != nil {
t.Fatalf("StartPlugins: %v", err)
}
if !svc.started {
t.Fatal("plugin Start was not called")
}

deps := svc.gotDeps
if deps.Streams == nil {
t.Error("Deps.Streams is nil")
}
if deps.Identity == nil {
t.Error("Deps.Identity is nil")
}
if deps.Events == nil {
t.Error("Deps.Events is nil")
}

// Exercise daemonIdentity adapter through Deps.Identity.
if got := deps.Identity.NodeID(); got != 0 {
t.Errorf("Identity.NodeID = %d, want 0", got)
}
addr := deps.Identity.Address()
if addr.Network != 0 || addr.Node != 0 {
t.Errorf("Identity.Address = %+v, want zero", addr)
}
if pk := deps.Identity.PublicKey(); pk != nil {
t.Errorf("Identity.PublicKey = %v, want nil (no identity)", pk)
}
if sig, err := deps.Identity.Sign([]byte("hi")); err == nil || sig != nil {
t.Errorf("Identity.Sign with no identity: got (%v, %v), want (nil, error)", sig, err)
}

// Exercise daemonEventBus adapter through Deps.Events.
// Publish should be safe with a real bus on the daemon.
deps.Events.Publish("test.evt", map[string]any{"k": "v"})
ch, cancelSub := deps.Events.Subscribe("test.*")
if ch == nil {
t.Fatal("Events.Subscribe returned nil channel")
}
cancelSub()

// Exercise daemonStreams adapter through Deps.Streams.
ln, err := deps.Streams.Listen(54322)
if err != nil {
t.Fatalf("Streams.Listen: %v", err)
}
if ln == nil {
t.Fatal("Listener is nil")
}
if ln.Port() != 54322 {
t.Errorf("Listener.Port = %d, want 54322", ln.Port())
}
// Addr on listener delegates to daemon.Addr — zero on fresh daemon.
_ = ln.Addr()
if err := ln.Close(); err != nil {
t.Errorf("Listener.Close: %v", err)
}

// Now Stop everything.
if err := rt.StopPlugins(context.Background()); err != nil {
t.Errorf("StopPlugins: %v", err)
}
if !svc.stopped {
t.Error("plugin Stop was not called")
}
}

// TestRuntime_StartPlugins_PropagatesError verifies a failing Start
// short-circuits and the error reaches the caller.
func TestRuntime_StartPlugins_PropagatesError(t *testing.T) {
t.Parallel()
d := daemon.New(daemon.Config{})
rt := runtime.New(d)

wantErr := errors.New("synthetic start failure")
svc := &fakeService{name: "broken", order: 50, startErr: wantErr}
if err := rt.Register(svc); err != nil {
t.Fatalf("Register: %v", err)
}

err := rt.StartPlugins(context.Background())
if !errors.Is(err, wantErr) {
t.Errorf("StartPlugins err = %v, want %v", err, wantErr)
}
}

// TestDaemonEventBus_PublishSubscribe exercises the bus adapter end-to-end:
// the subscriber goroutine should forward a daemon.Event to a coreapi.Event.
func TestDaemonEventBus_PublishSubscribe(t *testing.T) {
t.Parallel()
d := daemon.New(daemon.Config{})
rt := runtime.New(d)

svc := &fakeService{name: "fake", order: 100}
if err := rt.Register(svc); err != nil {
t.Fatalf("Register: %v", err)
}
if err := rt.StartPlugins(context.Background()); err != nil {
t.Fatalf("StartPlugins: %v", err)
}
t.Cleanup(func() { _ = rt.StopPlugins(context.Background()) })

bus := svc.gotDeps.Events
ch, cancel := bus.Subscribe("hello.*")
defer cancel()

// Publish before we read — the bus is buffered.
bus.Publish("hello.world", map[string]any{"x": 42})

select {
case ev := <-ch:
if ev.Topic != "hello.world" {
t.Errorf("Topic = %q, want hello.world", ev.Topic)
}
if v, _ := ev.Payload["x"].(int); v != 42 {
t.Errorf("Payload[x] = %v, want 42", ev.Payload["x"])
}
case <-time.After(500 * time.Millisecond):
t.Fatal("event not delivered within timeout")
}
}
50 changes: 50 additions & 0 deletions zz_eventbus_internal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// SPDX-License-Identifier: AGPL-3.0-or-later

package runtime

import (
"testing"
)

// TestDaemonEventBus_NilDaemon_Publish covers the defensive nil-daemon
// short-circuit in daemonEventBus.Publish. Constructing the adapter
// with a nil daemon (only reachable via direct package-internal use, but
// still a documented safety branch in events.go) must not panic.
func TestDaemonEventBus_NilDaemon_Publish(t *testing.T) {
t.Parallel()
bus := daemonEventBus{d: nil}
// Must be a no-op (no daemon to forward to). The branch is the
// `if b.d == nil { return }` guard.
bus.Publish("nil.daemon", map[string]any{"x": 1})
}

// TestDaemonEventBus_NilDaemon_Subscribe covers the nil-daemon branch of
// Subscribe — it returns a closed channel and a no-op cancel so callers
// never block. The interesting assertion is that the returned channel
// is already drained (recv returns the zero value with ok=false).
func TestDaemonEventBus_NilDaemon_Subscribe(t *testing.T) {
t.Parallel()
bus := daemonEventBus{d: nil}

ch, cancel := bus.Subscribe("anything.*")
if ch == nil {
t.Fatal("Subscribe returned nil channel")
}
if cancel == nil {
t.Fatal("Subscribe returned nil cancel")
}

// Channel should be closed — a receive must succeed with ok=false.
select {
case ev, ok := <-ch:
if ok {
t.Errorf("Subscribe(nil daemon): want closed channel, got event %+v", ev)
}
default:
t.Fatal("Subscribe(nil daemon): channel is not closed (would block)")
}

// Cancel must be safe to invoke and idempotent.
cancel()
cancel()
}
Loading
Loading