Say whether an approval was refused, unanswered, or never shown

This commit is contained in:
Viktor Liu
2026-08-31 15:15:37 +02:00
parent 3af7764b09
commit bad63c14b8
5 changed files with 200 additions and 10 deletions

View File

@@ -321,11 +321,29 @@ func (a *vncApprover) Request(ctx context.Context, info vncserver.ApprovalInfo)
Metadata: meta,
})
if err != nil {
return vncserver.ApprovalDecision{}, err
return vncserver.ApprovalDecision{}, approvalCause(err)
}
return vncserver.ApprovalDecision{ViewOnly: d.ViewOnly}, nil
}
// approvalCause restates a broker failure as the cause the VNC server
// classifies, so the peer that dialled learns whether the user refused or
// never answered. The broker's denial and timeout carry nothing beyond the
// sentinel, so those are returned as the server's own; the two unavailable
// cases differ in a way worth keeping, so they are wrapped.
func approvalCause(err error) error {
switch {
case errors.Is(err, approval.ErrDenied):
return vncserver.ErrApprovalDenied
case errors.Is(err, approval.ErrTimeout):
return vncserver.ErrApprovalTimeout
case errors.Is(err, approval.ErrNoSubscriber), errors.Is(err, approval.ErrPromptNotShown):
return fmt.Errorf("%w: %w", vncserver.ErrApprovalUnavailable, err)
default:
return err
}
}
func displayPeer(info vncserver.ApprovalInfo) string {
if info.Initiator != "" {
return info.Initiator

View File

@@ -0,0 +1,60 @@
//go:build !js && !ios && !android
package internal
import (
"context"
"errors"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/netbirdio/netbird/client/internal/approval"
vncserver "github.com/netbirdio/netbird/client/vnc/server"
)
// TestApprovalCause checks that each way the broker can refuse arrives at
// the VNC server as the matching cause. Without this the server sees one
// opaque error and tells every caller it was denied, which reads as "the
// user said no" even when no prompt was ever answered or shown.
func TestApprovalCause(t *testing.T) {
cases := []struct {
name string
in error
want error
}{
{"denied", approval.ErrDenied, vncserver.ErrApprovalDenied},
{"timeout", approval.ErrTimeout, vncserver.ErrApprovalTimeout},
{"no_subscriber", approval.ErrNoSubscriber, vncserver.ErrApprovalUnavailable},
{"prompt_not_shown", approval.ErrPromptNotShown, vncserver.ErrApprovalUnavailable},
// Wrapped on the way in: the broker may add context, and the
// classification must survive it.
{"wrapped_timeout", fmt.Errorf("request: %w", approval.ErrTimeout), vncserver.ErrApprovalTimeout},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := approvalCause(tc.in)
assert.ErrorIs(t, got, tc.want, "broker error %v must classify as %v", tc.in, tc.want)
})
}
}
// TestApprovalCausePreservesUnavailableDetail: the two unavailable causes
// mean different things to whoever reads the daemon log ("nobody was
// listening" versus "somebody was, and never got it"), so collapsing them
// onto one sentinel must not discard which one occurred.
func TestApprovalCausePreservesUnavailableDetail(t *testing.T) {
got := approvalCause(approval.ErrPromptNotShown)
assert.ErrorIs(t, got, vncserver.ErrApprovalUnavailable)
assert.ErrorIs(t, got, approval.ErrPromptNotShown, "the specific cause must stay readable in the log")
}
// TestApprovalCausePassesThroughUnknown keeps an error the mapping does not
// recognise intact, so the daemon log still shows what actually happened.
// The server rejects it as a denial either way.
func TestApprovalCausePassesThroughUnknown(t *testing.T) {
for _, err := range []error{context.Canceled, errors.New("something else")} {
assert.ErrorIs(t, approvalCause(err), err, "unknown cause must pass through unchanged")
}
}