|
|
|
@ -6,6 +6,7 @@ package cli
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"context"
|
|
|
|
|
"errors"
|
|
|
|
|
"flag"
|
|
|
|
|
"fmt"
|
|
|
|
|
"os"
|
|
|
|
@ -16,6 +17,7 @@ import (
|
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
"github.com/peterbourgon/ff/v3/ffcli"
|
|
|
|
|
"tailscale.com/client/tailscale"
|
|
|
|
|
"tailscale.com/ipn"
|
|
|
|
|
"tailscale.com/ipn/ipnstate"
|
|
|
|
|
"tailscale.com/tailcfg"
|
|
|
|
@ -745,6 +747,96 @@ func TestServeConfigMutations(t *testing.T) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestVerifyFunnelEnabled(t *testing.T) {
|
|
|
|
|
lc := &fakeLocalServeClient{}
|
|
|
|
|
var stdout bytes.Buffer
|
|
|
|
|
var flagOut bytes.Buffer
|
|
|
|
|
e := &serveEnv{
|
|
|
|
|
lc: lc,
|
|
|
|
|
testFlagOut: &flagOut,
|
|
|
|
|
testStdout: &stdout,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
// queryFeatureResponse is the mock response desired from the
|
|
|
|
|
// call made to lc.QueryFeature by verifyFunnelEnabled.
|
|
|
|
|
queryFeatureResponse mockQueryFeatureResponse
|
|
|
|
|
caps []string // optionally set at fakeStatus.Capabilities
|
|
|
|
|
wantErr string
|
|
|
|
|
wantPanic string
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
name: "enabled",
|
|
|
|
|
queryFeatureResponse: mockQueryFeatureResponse{resp: &tailcfg.QueryFeatureResponse{Complete: true}, err: nil},
|
|
|
|
|
wantErr: "", // no error, success
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "fallback-to-non-interactive-flow",
|
|
|
|
|
queryFeatureResponse: mockQueryFeatureResponse{resp: nil, err: errors.New("not-allowed")},
|
|
|
|
|
wantErr: "Funnel not available; HTTPS must be enabled. See https://tailscale.com/s/https.",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "fallback-flow-missing-acl-rule",
|
|
|
|
|
queryFeatureResponse: mockQueryFeatureResponse{resp: nil, err: errors.New("not-allowed")},
|
|
|
|
|
caps: []string{tailcfg.CapabilityHTTPS},
|
|
|
|
|
wantErr: `Funnel not available; "funnel" node attribute not set. See https://tailscale.com/s/no-funnel.`,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "fallback-flow-enabled",
|
|
|
|
|
queryFeatureResponse: mockQueryFeatureResponse{resp: nil, err: errors.New("not-allowed")},
|
|
|
|
|
caps: []string{tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel},
|
|
|
|
|
wantErr: "", // no error, success
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "not-allowed-to-enable",
|
|
|
|
|
queryFeatureResponse: mockQueryFeatureResponse{resp: &tailcfg.QueryFeatureResponse{
|
|
|
|
|
Complete: false,
|
|
|
|
|
Text: "You don't have permission to enable this feature.",
|
|
|
|
|
ShouldWait: false,
|
|
|
|
|
}, err: nil},
|
|
|
|
|
wantErr: "",
|
|
|
|
|
wantPanic: "unexpected call to os.Exit(0) during test", // os.Exit(0) should be called to end process
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
lc.setQueryFeatureResponse(tt.queryFeatureResponse)
|
|
|
|
|
|
|
|
|
|
if tt.caps != nil {
|
|
|
|
|
oldCaps := fakeStatus.Self.Capabilities
|
|
|
|
|
defer func() { fakeStatus.Self.Capabilities = oldCaps }() // reset after test
|
|
|
|
|
fakeStatus.Self.Capabilities = tt.caps
|
|
|
|
|
}
|
|
|
|
|
st, err := e.getLocalClientStatus(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
defer func() {
|
|
|
|
|
r := recover()
|
|
|
|
|
var gotPanic string
|
|
|
|
|
if r != nil {
|
|
|
|
|
gotPanic = fmt.Sprint(r)
|
|
|
|
|
}
|
|
|
|
|
if gotPanic != tt.wantPanic {
|
|
|
|
|
t.Errorf("wrong panic; got=%s, want=%s", gotPanic, tt.wantPanic)
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
gotErr := e.verifyFunnelEnabled(ctx, st, 443)
|
|
|
|
|
var got string
|
|
|
|
|
if gotErr != nil {
|
|
|
|
|
got = gotErr.Error()
|
|
|
|
|
}
|
|
|
|
|
if got != tt.wantErr {
|
|
|
|
|
t.Errorf("wrong error; got=%s, want=%s", gotErr, tt.wantErr)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// fakeLocalServeClient is a fake tailscale.LocalClient for tests.
|
|
|
|
|
// It's not a full implementation, just enough to test the serve command.
|
|
|
|
|
//
|
|
|
|
@ -753,6 +845,7 @@ func TestServeConfigMutations(t *testing.T) {
|
|
|
|
|
type fakeLocalServeClient struct {
|
|
|
|
|
config *ipn.ServeConfig
|
|
|
|
|
setCount int // counts calls to SetServeConfig
|
|
|
|
|
queryFeatureResponse *mockQueryFeatureResponse // mock response to QueryFeature calls
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// fakeStatus is a fake ipnstate.Status value for tests.
|
|
|
|
@ -782,7 +875,24 @@ func (lc *fakeLocalServeClient) SetServeConfig(ctx context.Context, config *ipn.
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (lc *fakeLocalServeClient) QueryFeature(context.Context, string) (*tailcfg.QueryFeatureResponse, error) {
|
|
|
|
|
type mockQueryFeatureResponse struct {
|
|
|
|
|
resp *tailcfg.QueryFeatureResponse
|
|
|
|
|
err error
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (lc *fakeLocalServeClient) setQueryFeatureResponse(resp mockQueryFeatureResponse) {
|
|
|
|
|
lc.queryFeatureResponse = &resp
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (lc *fakeLocalServeClient) QueryFeature(ctx context.Context, feature string) (*tailcfg.QueryFeatureResponse, error) {
|
|
|
|
|
if resp := lc.queryFeatureResponse; resp != nil {
|
|
|
|
|
// If we're testing QueryFeature, use the response value set for the test.
|
|
|
|
|
return resp.resp, resp.err
|
|
|
|
|
}
|
|
|
|
|
return &tailcfg.QueryFeatureResponse{Complete: true}, nil // fallback to already enabled
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (lc *fakeLocalServeClient) WatchIPNBus(ctx context.Context, mask ipn.NotifyWatchOpt) (*tailscale.IPNBusWatcher, error) {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|