3ec5be3f51
This file was never truly necessary and has never actually been used in the history of Tailscale's open source releases. A Brief History of AUTHORS files --- The AUTHORS file was a pattern developed at Google, originally for Chromium, then adopted by Go and a bunch of other projects. The problem was that Chromium originally had a copyright line only recognizing Google as the copyright holder. Because Google (and most open source projects) do not require copyright assignemnt for contributions, each contributor maintains their copyright. Some large corporate contributors then tried to add their own name to the copyright line in the LICENSE file or in file headers. This quickly becomes unwieldy, and puts a tremendous burden on anyone building on top of Chromium, since the license requires that they keep all copyright lines intact. The compromise was to create an AUTHORS file that would list all of the copyright holders. The LICENSE file and source file headers would then include that list by reference, listing the copyright holder as "The Chromium Authors". This also become cumbersome to simply keep the file up to date with a high rate of new contributors. Plus it's not always obvious who the copyright holder is. Sometimes it is the individual making the contribution, but many times it may be their employer. There is no way for the proejct maintainer to know. Eventually, Google changed their policy to no longer recommend trying to keep the AUTHORS file up to date proactively, and instead to only add to it when requested: https://opensource.google/docs/releasing/authors. They are also clear that: > Adding contributors to the AUTHORS file is entirely within the > project's discretion and has no implications for copyright ownership. It was primarily added to appease a small number of large contributors that insisted that they be recognized as copyright holders (which was entirely their right to do). But it's not truly necessary, and not even the most accurate way of identifying contributors and/or copyright holders. In practice, we've never added anyone to our AUTHORS file. It only lists Tailscale, so it's not really serving any purpose. It also causes confusion because Tailscalars put the "Tailscale Inc & AUTHORS" header in other open source repos which don't actually have an AUTHORS file, so it's ambiguous what that means. Instead, we just acknowledge that the contributors to Tailscale (whoever they are) are copyright holders for their individual contributions. We also have the benefit of using the DCO (developercertificate.org) which provides some additional certification of their right to make the contribution. The source file changes were purely mechanical with: git ls-files | xargs sed -i -e 's/\(Tailscale Inc &\) AUTHORS/\1 contributors/g' Updates #cleanup Change-Id: Ia101a4a3005adb9118051b3416f5a64a4a45987d Signed-off-by: Will Norris <will@tailscale.com>
2363 lines
65 KiB
Go
2363 lines
65 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/netip"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"regexp"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/peterbourgon/ff/v3/ffcli"
|
|
"tailscale.com/ipn"
|
|
"tailscale.com/ipn/ipnstate"
|
|
"tailscale.com/tailcfg"
|
|
"tailscale.com/types/views"
|
|
)
|
|
|
|
func TestServeDevConfigMutations(t *testing.T) {
|
|
// step is a stateful mutation within a group
|
|
type step struct {
|
|
command []string // serve args; nil means no command to run (only reset)
|
|
want *ipn.ServeConfig // non-nil means we want a save of this value
|
|
wantErr func(error) (badErrMsg string) // nil means no error is wanted
|
|
}
|
|
|
|
// group is a group of steps that share the same
|
|
// config mutation
|
|
type group struct {
|
|
name string
|
|
steps []step
|
|
initialState fakeLocalServeClient // use the zero value for empty config
|
|
}
|
|
|
|
// creaet a temporary directory for path-based destinations
|
|
td := t.TempDir()
|
|
writeFile := func(suffix, contents string) {
|
|
if err := os.WriteFile(filepath.Join(td, suffix), []byte(contents), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
writeFile("foo", "this is foo")
|
|
err := os.MkdirAll(filepath.Join(td, "subdir"), 0700)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
writeFile("subdir/file-a", "this is subdir")
|
|
|
|
groups := [...]group{
|
|
{
|
|
name: "using_port_number",
|
|
steps: []step{{
|
|
command: cmd("funnel --bg 3000"),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://127.0.0.1:3000"},
|
|
}},
|
|
},
|
|
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true},
|
|
},
|
|
}},
|
|
},
|
|
{
|
|
name: "funnel_background",
|
|
steps: []step{{
|
|
command: cmd("funnel --bg localhost:3000"),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://localhost:3000"},
|
|
}},
|
|
},
|
|
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true},
|
|
},
|
|
}},
|
|
},
|
|
{
|
|
name: "serve_background",
|
|
steps: []step{{
|
|
command: cmd("serve --bg localhost:3000"),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://localhost:3000"},
|
|
}},
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
{
|
|
name: "set_path_bg",
|
|
steps: []step{{
|
|
command: cmd("serve --set-path=/ --bg localhost:3000"),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://localhost:3000"},
|
|
}},
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
{
|
|
name: "http_listener",
|
|
steps: []step{{
|
|
command: cmd("serve --bg --http=80 localhost:3000"),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://localhost:3000"},
|
|
}},
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
{
|
|
name: "https_listener_valid_port",
|
|
steps: []step{{
|
|
command: cmd("serve --bg --https=8443 localhost:3000"),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{8443: {HTTPS: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://localhost:3000"},
|
|
}},
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
{
|
|
name: "multiple_http_with_off",
|
|
steps: []step{
|
|
{
|
|
command: cmd("serve --http=80 --bg http://localhost:3000"),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://localhost:3000"},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
{ // support non Funnel port
|
|
command: cmd("serve --bg --http=9999 --set-path=/abc http://localhost:3001"),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}, 9999: {HTTP: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://localhost:3000"},
|
|
}},
|
|
"foo.test.ts.net:9999": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/abc": {Proxy: "http://localhost:3001"},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
{ // turn off one handler
|
|
command: cmd("serve --bg --http=9999 --set-path=/abc off"),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://localhost:3000"},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
{ // add another handler
|
|
command: cmd("serve --bg --http=8080 --set-path=/abc http://127.0.0.1:3001"),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}, 8080: {HTTP: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://localhost:3000"},
|
|
}},
|
|
"foo.test.ts.net:8080": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/abc": {Proxy: "http://127.0.0.1:3001"},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "invalid_port_too_low",
|
|
steps: []step{{
|
|
command: cmd("serve --https=443 --bg http://localhost:0"), // invalid port, too low
|
|
wantErr: anyErr(),
|
|
}},
|
|
},
|
|
{
|
|
name: "invalid_port_too_high",
|
|
steps: []step{{
|
|
command: cmd("serve --https=443 --bg http://localhost:65536"), // invalid port, too high
|
|
wantErr: anyErr(),
|
|
}},
|
|
},
|
|
{
|
|
name: "invalid_mount_port_too_high",
|
|
steps: []step{{
|
|
command: cmd("serve --https=65536 --bg http://localhost:3000"), // invalid port, too high
|
|
wantErr: anyErr(),
|
|
}},
|
|
},
|
|
{
|
|
name: "ip_host",
|
|
initialState: fakeLocalServeClient{
|
|
SOMarkInUse: true,
|
|
},
|
|
steps: []step{{
|
|
command: cmd("serve --https=443 --bg http://192.168.1.1:3000"),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://192.168.1.1:3000"},
|
|
}},
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
{
|
|
name: "invalid_scheme",
|
|
steps: []step{{
|
|
command: cmd("serve --https=443 --bg httpz://127.0.0.1"), // invalid scheme
|
|
wantErr: anyErr(),
|
|
}},
|
|
},
|
|
{
|
|
name: "no_scheme_remote_host_tcp",
|
|
initialState: fakeLocalServeClient{
|
|
SOMarkInUse: true,
|
|
},
|
|
steps: []step{{
|
|
command: cmd("serve --https=443 --bg 192.168.1.1:3000"),
|
|
wantErr: exactErrMsg(errHelp),
|
|
}},
|
|
},
|
|
{
|
|
name: "turn_off_https",
|
|
steps: []step{
|
|
{
|
|
command: cmd("serve --bg --https=443 http://localhost:3000"),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://localhost:3000"},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
command: cmd("serve --bg --https=9999 --set-path=/abc http://localhost:3001"),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 9999: {HTTPS: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://localhost:3000"},
|
|
}},
|
|
"foo.test.ts.net:9999": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/abc": {Proxy: "http://localhost:3001"},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
command: cmd("serve --bg --https=9999 --set-path=/abc off"),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://localhost:3000"},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
command: cmd("serve --bg --https=8443 --set-path=/abc http://127.0.0.1:3001"),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://localhost:3000"},
|
|
}},
|
|
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/abc": {Proxy: "http://127.0.0.1:3001"},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "https_text_bg",
|
|
steps: []step{{
|
|
command: cmd("serve --bg --https=10000 text:hi"),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{10000: {HTTPS: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:10000": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Text: "hi"},
|
|
}},
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
{
|
|
name: "handler_not_found",
|
|
steps: []step{{
|
|
command: cmd("serve --https=443 --set-path=/foo off"),
|
|
want: nil, // nothing to save
|
|
wantErr: anyErr(),
|
|
}},
|
|
},
|
|
{
|
|
name: "clean_mount", // "bar" becomes "/bar"
|
|
steps: []step{{
|
|
command: cmd("serve --bg --https=443 --set-path=bar https://127.0.0.1:8443"),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/bar": {Proxy: "https://127.0.0.1:8443"},
|
|
}},
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
{
|
|
name: "serve_reset",
|
|
steps: []step{
|
|
{
|
|
command: cmd("serve --bg --https=443 --set-path=bar https://127.0.0.1:8443"),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/bar": {Proxy: "https://127.0.0.1:8443"},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
command: cmd("serve reset"),
|
|
want: &ipn.ServeConfig{},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "https_insecure",
|
|
steps: []step{{
|
|
command: cmd("serve --bg --https=443 https+insecure://127.0.0.1:3001"),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "https+insecure://127.0.0.1:3001"},
|
|
}},
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
{
|
|
name: "two_ports_same_dest",
|
|
steps: []step{
|
|
{
|
|
command: cmd("serve --bg --https=443 --set-path=/foo localhost:3000"),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/foo": {Proxy: "http://localhost:3000"},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
command: cmd("serve --bg --https=8443 --set-path=/foo localhost:3000"),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/foo": {Proxy: "http://localhost:3000"},
|
|
}},
|
|
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/foo": {Proxy: "http://localhost:3000"},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "path_in_dest",
|
|
steps: []step{{
|
|
command: cmd("serve --bg --https=443 http://127.0.0.1:3000/foo/bar"),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://127.0.0.1:3000/foo/bar"},
|
|
}},
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
{
|
|
name: "tcp_port_too_low",
|
|
initialState: fakeLocalServeClient{
|
|
SOMarkInUse: true,
|
|
},
|
|
steps: []step{{
|
|
command: cmd("serve --tls-terminated-tcp=443 --bg tcp://somehost:0"),
|
|
wantErr: exactErrMsg(errHelp),
|
|
}},
|
|
},
|
|
{
|
|
name: "tcp_port_too_high",
|
|
initialState: fakeLocalServeClient{
|
|
SOMarkInUse: true,
|
|
},
|
|
steps: []step{{
|
|
command: cmd("serve --tls-terminated-tcp=443 --bg tcp://somehost:65536"),
|
|
wantErr: exactErrMsg(errHelp),
|
|
}},
|
|
},
|
|
{
|
|
name: "tcp_shorthand",
|
|
steps: []step{{
|
|
command: cmd("serve --tls-terminated-tcp=443 --bg 5432"),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
|
443: {
|
|
TCPForward: "127.0.0.1:5432",
|
|
TerminateTLS: "foo.test.ts.net",
|
|
},
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
{
|
|
name: "tls_terminated_tcp",
|
|
steps: []step{
|
|
{
|
|
command: cmd("serve --tls-terminated-tcp=443 --bg tcp://localhost:5432"),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
|
443: {
|
|
TCPForward: "localhost:5432",
|
|
TerminateTLS: "foo.test.ts.net",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
command: cmd("serve --tls-terminated-tcp=443 --bg tcp://127.0.0.1:8443"),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
|
443: {
|
|
TCPForward: "127.0.0.1:8443",
|
|
TerminateTLS: "foo.test.ts.net",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "tcp_off",
|
|
steps: []step{
|
|
{
|
|
command: cmd("serve --tls-terminated-tcp=443 --bg tcp://localhost:123"),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
|
443: {
|
|
TCPForward: "localhost:123",
|
|
TerminateTLS: "foo.test.ts.net",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{ // handler doesn't exist
|
|
command: cmd("serve --tls-terminated-tcp=8443 off"),
|
|
wantErr: anyErr(),
|
|
},
|
|
{
|
|
command: cmd("serve --tls-terminated-tcp=443 off"),
|
|
want: &ipn.ServeConfig{},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "text",
|
|
steps: []step{{
|
|
command: cmd("serve --https=443 --bg text:hello"),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Text: "hello"},
|
|
}},
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
{
|
|
name: "path",
|
|
steps: []step{
|
|
{
|
|
command: cmd("serve --https=443 --bg " + filepath.Join(td, "foo")),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Path: filepath.Join(td, "foo")},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
command: cmd("serve --bg --https=443 --set-path=/some/where " + filepath.Join(td, "subdir/file-a")),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Path: filepath.Join(td, "foo")},
|
|
"/some/where": {Path: filepath.Join(td, "subdir/file-a")},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "bad_path",
|
|
initialState: fakeLocalServeClient{
|
|
SOMarkInUse: true,
|
|
},
|
|
steps: []step{{
|
|
command: cmd("serve --bg --https=443 bad/path"),
|
|
wantErr: exactErrMsg(errHelp),
|
|
}},
|
|
},
|
|
{
|
|
name: "path_off",
|
|
steps: []step{
|
|
{
|
|
command: cmd("serve --bg --https=443 " + filepath.Join(td, "subdir")),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Path: filepath.Join(td, "subdir/")},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
command: cmd("serve --bg --https=443 off"),
|
|
want: &ipn.ServeConfig{},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "combos",
|
|
steps: []step{
|
|
{
|
|
command: cmd("serve --bg localhost:3000"),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://localhost:3000"},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
{ // enable funnel for primary port
|
|
command: cmd("funnel --bg localhost:3000"),
|
|
want: &ipn.ServeConfig{
|
|
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true},
|
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://localhost:3000"},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
{ // serving on secondary port doesn't change funnel on primary port
|
|
command: cmd("serve --bg --https=8443 --set-path=/bar localhost:3001"),
|
|
want: &ipn.ServeConfig{
|
|
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true},
|
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://localhost:3000"},
|
|
}},
|
|
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/bar": {Proxy: "http://localhost:3001"},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
{ // turn funnel on for secondary port
|
|
command: cmd("funnel --bg --https=8443 --set-path=/bar localhost:3001"),
|
|
want: &ipn.ServeConfig{
|
|
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true, "foo.test.ts.net:8443": true},
|
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://localhost:3000"},
|
|
}},
|
|
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/bar": {Proxy: "http://localhost:3001"},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
{ // turn funnel off for primary port 443
|
|
command: cmd("serve --bg localhost:3000"),
|
|
want: &ipn.ServeConfig{
|
|
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
|
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://localhost:3000"},
|
|
}},
|
|
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/bar": {Proxy: "http://localhost:3001"},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
{ // remove secondary port
|
|
command: cmd("serve --bg --https=8443 --set-path=/bar off"),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://localhost:3000"},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
{ // start a tcp forwarder on 8443
|
|
command: cmd("serve --bg --tcp=8443 tcp://localhost:5432"),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {TCPForward: "localhost:5432"}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://localhost:3000"},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
{ // remove primary port http handler
|
|
command: cmd("serve off"),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{8443: {TCPForward: "localhost:5432"}},
|
|
},
|
|
},
|
|
{ // remove tcp forwarder
|
|
command: cmd("serve --tls-terminated-tcp=8443 off"),
|
|
want: &ipn.ServeConfig{},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "tricky_steps",
|
|
steps: []step{
|
|
{ // a directory with a trailing slash mount point
|
|
command: cmd("serve --bg --https=443 --set-path=/dir " + filepath.Join(td, "subdir")),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/dir/": {Path: filepath.Join(td, "subdir/")},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
{ // this should overwrite the previous one
|
|
command: cmd("serve --bg --https=443 --set-path=/dir " + filepath.Join(td, "foo")),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/dir": {Path: filepath.Join(td, "foo")},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
{ // reset and do opposite
|
|
command: cmd("serve reset"),
|
|
want: &ipn.ServeConfig{},
|
|
},
|
|
{ // a file without a trailing slash mount point
|
|
command: cmd("serve --bg --https=443 --set-path=/dir " + filepath.Join(td, "foo")),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/dir": {Path: filepath.Join(td, "foo")},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
{ // this should overwrite the previous one
|
|
command: cmd("serve --bg --https=443 --set-path=/dir " + filepath.Join(td, "subdir")),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/dir/": {Path: filepath.Join(td, "subdir/")},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "cannot_override_tcp_with_http",
|
|
steps: []step{
|
|
{ // tcp forward 5432 on serve port 443
|
|
command: cmd("serve --tls-terminated-tcp=443 --bg tcp://localhost:5432"),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
|
443: {
|
|
TCPForward: "localhost:5432",
|
|
TerminateTLS: "foo.test.ts.net",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
command: cmd("serve --https=443 --bg localhost:3000"),
|
|
wantErr: anyErr(),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "cannot_override_http_with_tcp",
|
|
steps: []step{
|
|
{
|
|
command: cmd("serve --https=443 --bg localhost:3000"),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://localhost:3000"},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
{ // try to start a tcp forwarder on the same serve port
|
|
command: cmd("serve --tls-terminated-tcp=443 --bg tcp://localhost:5432"),
|
|
wantErr: anyErr(),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "turn_off_multiple_handlers",
|
|
steps: []step{
|
|
{
|
|
command: cmd("serve --https=4545 --set-path=/foo --bg localhost:3000"),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{4545: {HTTPS: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:4545": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/foo": {Proxy: "http://localhost:3000"},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
command: cmd("serve --https=4545 --set-path=/bar --bg localhost:3000"),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{4545: {HTTPS: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:4545": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/foo": {Proxy: "http://localhost:3000"},
|
|
"/bar": {Proxy: "http://localhost:3000"},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
command: cmd("serve --https=4545 --bg --yes localhost:3000 off"),
|
|
want: &ipn.ServeConfig{},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "no_http_with_funnel",
|
|
steps: []step{
|
|
{
|
|
command: cmd("funnel --http=80 3000"),
|
|
// error parsing commandline arguments: flag provided but not defined: -http
|
|
wantErr: anyErr(),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "advertise_service",
|
|
initialState: fakeLocalServeClient{
|
|
statusWithoutPeers: &ipnstate.Status{
|
|
BackendState: ipn.Running.String(),
|
|
Self: &ipnstate.PeerStatus{
|
|
DNSName: "foo.test.ts.net",
|
|
CapMap: tailcfg.NodeCapMap{
|
|
tailcfg.NodeAttrFunnel: nil,
|
|
tailcfg.CapabilityFunnelPorts + "?ports=443,8443": nil,
|
|
},
|
|
Tags: ptrToReadOnlySlice([]string{"some-tag"}),
|
|
},
|
|
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
|
|
},
|
|
SOMarkInUse: true,
|
|
},
|
|
steps: []step{{
|
|
command: cmd("serve --service=svc:foo --http=80 text:foo"),
|
|
want: &ipn.ServeConfig{
|
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
|
"svc:foo": {
|
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
|
80: {HTTP: true},
|
|
},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Text: "foo"},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
{
|
|
name: "advertise_service_from_untagged_node",
|
|
steps: []step{{
|
|
command: cmd("serve --service=svc:foo --http=80 text:foo"),
|
|
wantErr: anyErr(),
|
|
}},
|
|
},
|
|
{
|
|
name: "forward_grant_header",
|
|
steps: []step{
|
|
{
|
|
command: cmd("serve --bg --accept-app-caps=example.com/cap/foo 3000"),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {
|
|
Proxy: "http://127.0.0.1:3000",
|
|
AcceptAppCaps: []tailcfg.PeerCapability{"example.com/cap/foo"},
|
|
},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
command: cmd("serve --bg --accept-app-caps=example.com/cap/foo,example.com/cap/bar 3000"),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {
|
|
Proxy: "http://127.0.0.1:3000",
|
|
AcceptAppCaps: []tailcfg.PeerCapability{"example.com/cap/foo", "example.com/cap/bar"},
|
|
},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
command: cmd("serve --bg --accept-app-caps=example.com/cap/bar 3000"),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {
|
|
Proxy: "http://127.0.0.1:3000",
|
|
AcceptAppCaps: []tailcfg.PeerCapability{"example.com/cap/bar"},
|
|
},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "invalid_accept_caps_invalid_app_cap",
|
|
steps: []step{
|
|
{
|
|
command: cmd("serve --bg --accept-app-caps=example.com/cap/fine,NOTFINE 3000"), // should be {domain.tld}/{name}
|
|
wantErr: func(err error) (badErrMsg string) {
|
|
if err == nil || !strings.Contains(err.Error(), fmt.Sprintf("%q does not match", "NOTFINE")) {
|
|
return fmt.Sprintf("wanted validation error that quotes the non-matching capability (and nothing more) but got %q", err.Error())
|
|
}
|
|
return ""
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "tcp_with_proxy_protocol_v1",
|
|
steps: []step{{
|
|
command: cmd("serve --tcp=8000 --proxy-protocol=1 --bg tcp://localhost:5432"),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
|
8000: {
|
|
TCPForward: "localhost:5432",
|
|
ProxyProtocol: 1,
|
|
},
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
{
|
|
name: "tls_terminated_tcp_with_proxy_protocol_v2",
|
|
steps: []step{{
|
|
command: cmd("serve --tls-terminated-tcp=443 --proxy-protocol=2 --bg tcp://localhost:5432"),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
|
443: {
|
|
TCPForward: "localhost:5432",
|
|
TerminateTLS: "foo.test.ts.net",
|
|
ProxyProtocol: 2,
|
|
},
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
{
|
|
name: "tcp_update_to_add_proxy_protocol",
|
|
steps: []step{
|
|
{
|
|
command: cmd("serve --tcp=8000 --bg tcp://localhost:5432"),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
|
8000: {TCPForward: "localhost:5432"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
command: cmd("serve --tcp=8000 --proxy-protocol=1 --bg tcp://localhost:5432"),
|
|
want: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
|
8000: {
|
|
TCPForward: "localhost:5432",
|
|
ProxyProtocol: 1,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "tcp_proxy_protocol_invalid_version",
|
|
steps: []step{{
|
|
command: cmd("serve --tcp=8000 --proxy-protocol=3 --bg tcp://localhost:5432"),
|
|
wantErr: anyErr(),
|
|
}},
|
|
},
|
|
{
|
|
name: "proxy_protocol_without_tcp",
|
|
steps: []step{{
|
|
command: cmd("serve --https=443 --proxy-protocol=1 --bg http://localhost:3000"),
|
|
wantErr: anyErr(),
|
|
}},
|
|
},
|
|
}
|
|
|
|
for _, group := range groups {
|
|
t.Run(group.name, func(t *testing.T) {
|
|
lc := group.initialState
|
|
for i, st := range group.steps {
|
|
var stderr bytes.Buffer
|
|
var stdout bytes.Buffer
|
|
var flagOut bytes.Buffer
|
|
e := &serveEnv{
|
|
lc: &lc,
|
|
testFlagOut: &flagOut,
|
|
testStdout: &stdout,
|
|
testStderr: &stderr,
|
|
}
|
|
lastCount := lc.setCount
|
|
var cmd *ffcli.Command
|
|
var args []string
|
|
|
|
mode := serve
|
|
if st.command[0] == "funnel" {
|
|
mode = funnel
|
|
}
|
|
cmd = newServeV2Command(e, mode)
|
|
args = st.command[1:]
|
|
|
|
err := cmd.ParseAndRun(context.Background(), args)
|
|
if flagOut.Len() > 0 {
|
|
t.Logf("flag package output: %q", flagOut.Bytes())
|
|
}
|
|
if err != nil {
|
|
if st.wantErr == nil {
|
|
t.Fatalf("step #%d: unexpected error: %v", i, err)
|
|
}
|
|
if bad := st.wantErr(err); bad != "" {
|
|
t.Fatalf("step #%d: unexpected error: %v", i, bad)
|
|
}
|
|
continue
|
|
}
|
|
if st.wantErr != nil {
|
|
t.Fatalf("step #%d: got success (saved=%v), but wanted an error", i, lc.config != nil)
|
|
}
|
|
var got *ipn.ServeConfig = nil
|
|
if lc.setCount > lastCount {
|
|
got = lc.config
|
|
}
|
|
if !reflect.DeepEqual(got, st.want) {
|
|
gotbts, _ := json.MarshalIndent(got, "", "\t")
|
|
wantbts, _ := json.MarshalIndent(st.want, "", "\t")
|
|
t.Fatalf("step: %d, cmd: %v, diff:\n%s", i, st.command, cmp.Diff(string(gotbts), string(wantbts)))
|
|
|
|
}
|
|
}
|
|
})
|
|
|
|
}
|
|
}
|
|
|
|
func TestSrcTypeFromFlags(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
env *serveEnv
|
|
expectedType serveType
|
|
expectedPort uint16
|
|
expectedErr bool
|
|
}{
|
|
{
|
|
name: "only http set",
|
|
env: &serveEnv{http: 80},
|
|
expectedType: serveTypeHTTP,
|
|
expectedPort: 80,
|
|
expectedErr: false,
|
|
},
|
|
{
|
|
name: "only https set",
|
|
env: &serveEnv{https: 10000},
|
|
expectedType: serveTypeHTTPS,
|
|
expectedPort: 10000,
|
|
expectedErr: false,
|
|
},
|
|
{
|
|
name: "only tcp set",
|
|
env: &serveEnv{tcp: 8000},
|
|
expectedType: serveTypeTCP,
|
|
expectedPort: 8000,
|
|
expectedErr: false,
|
|
},
|
|
{
|
|
name: "only tls-terminated-tcp set",
|
|
env: &serveEnv{tlsTerminatedTCP: 8080},
|
|
expectedType: serveTypeTLSTerminatedTCP,
|
|
expectedPort: 8080,
|
|
expectedErr: false,
|
|
},
|
|
{
|
|
name: "defaults to https, port 443",
|
|
env: &serveEnv{},
|
|
expectedType: serveTypeHTTPS,
|
|
expectedPort: 443,
|
|
expectedErr: false,
|
|
},
|
|
{
|
|
name: "defaults to https, port 443 for service",
|
|
env: &serveEnv{service: "svc:foo"},
|
|
expectedType: serveTypeHTTPS,
|
|
expectedPort: 443,
|
|
expectedErr: false,
|
|
},
|
|
{
|
|
name: "multiple types set",
|
|
env: &serveEnv{http: 80, https: 443},
|
|
expectedPort: 0,
|
|
expectedErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
srcType, srcPort, err := srvTypeAndPortFromFlags(tt.env)
|
|
if (err != nil) != tt.expectedErr {
|
|
t.Errorf("Expected error: %v, got: %v", tt.expectedErr, err)
|
|
}
|
|
if srcType != tt.expectedType {
|
|
t.Errorf("Expected srcType: %s, got: %s", tt.expectedType.String(), srcType)
|
|
}
|
|
if srcPort != tt.expectedPort {
|
|
t.Errorf("Expected srcPort: %d, got: %d", tt.expectedPort, srcPort)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAcceptSetAppCapsFlag(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
inputs []string
|
|
expectErr bool
|
|
expectErrToMatch *regexp.Regexp
|
|
expectedValue []tailcfg.PeerCapability
|
|
}{
|
|
{
|
|
name: "valid_simple",
|
|
inputs: []string{"example.com/name"},
|
|
expectErr: false,
|
|
expectedValue: []tailcfg.PeerCapability{"example.com/name"},
|
|
},
|
|
{
|
|
name: "valid_unicode",
|
|
inputs: []string{"bücher.de/something"},
|
|
expectErr: false,
|
|
expectedValue: []tailcfg.PeerCapability{"bücher.de/something"},
|
|
},
|
|
{
|
|
name: "more_valid_unicode",
|
|
inputs: []string{"example.tw/某某某"},
|
|
expectErr: false,
|
|
expectedValue: []tailcfg.PeerCapability{"example.tw/某某某"},
|
|
},
|
|
{
|
|
name: "valid_path_slashes",
|
|
inputs: []string{"domain.com/path/to/name"},
|
|
expectErr: false,
|
|
expectedValue: []tailcfg.PeerCapability{"domain.com/path/to/name"},
|
|
},
|
|
{
|
|
name: "valid_multiple_sets",
|
|
inputs: []string{"one.com/foo,two.com/bar"},
|
|
expectErr: false,
|
|
expectedValue: []tailcfg.PeerCapability{"one.com/foo", "two.com/bar"},
|
|
},
|
|
{
|
|
name: "valid_empty_string",
|
|
inputs: []string{""},
|
|
expectErr: false,
|
|
expectedValue: nil, // Empty string should be a no-op and not append anything.
|
|
},
|
|
{
|
|
name: "invalid_path_chars",
|
|
inputs: []string{"domain.com/path_with_underscore"},
|
|
expectErr: true,
|
|
expectErrToMatch: regexp.MustCompile(`"domain.com/path_with_underscore"`),
|
|
expectedValue: nil, // Slice should remain empty.
|
|
},
|
|
{
|
|
name: "valid_subdomain",
|
|
inputs: []string{"sub.domain.com/name"},
|
|
expectErr: false,
|
|
expectedValue: []tailcfg.PeerCapability{"sub.domain.com/name"},
|
|
},
|
|
{
|
|
name: "invalid_no_path",
|
|
inputs: []string{"domain.com/"},
|
|
expectErr: true,
|
|
expectErrToMatch: regexp.MustCompile(`"domain.com/"`),
|
|
expectedValue: nil,
|
|
},
|
|
{
|
|
name: "invalid_no_domain",
|
|
inputs: []string{"/path/only"},
|
|
expectErr: true,
|
|
expectErrToMatch: regexp.MustCompile(`"/path/only"`),
|
|
expectedValue: nil,
|
|
},
|
|
{
|
|
name: "some_invalid_some_valid",
|
|
inputs: []string{"one.com/foo,bad/bar,two.com/baz"},
|
|
expectErr: true,
|
|
expectErrToMatch: regexp.MustCompile(`"bad/bar"`),
|
|
expectedValue: []tailcfg.PeerCapability{"one.com/foo"}, // Parsing will stop after first error
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var v []tailcfg.PeerCapability
|
|
flag := &acceptAppCapsFlag{Value: &v}
|
|
|
|
var err error
|
|
for _, s := range tc.inputs {
|
|
err = flag.Set(s)
|
|
if err != nil {
|
|
break
|
|
}
|
|
}
|
|
|
|
if tc.expectErr && err == nil {
|
|
t.Errorf("expected an error, but got none")
|
|
}
|
|
if tc.expectErrToMatch != nil {
|
|
if !tc.expectErrToMatch.MatchString(err.Error()) {
|
|
t.Errorf("expected error to match %q, but was %q", tc.expectErrToMatch, err)
|
|
}
|
|
}
|
|
if !tc.expectErr && err != nil {
|
|
t.Errorf("did not expect an error, but got: %v", err)
|
|
}
|
|
|
|
if !reflect.DeepEqual(tc.expectedValue, v) {
|
|
t.Errorf("unexpected value, got: %q, want: %q", v, tc.expectedValue)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCleanURLPath(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expected string
|
|
wantErr bool
|
|
}{
|
|
{input: "", expected: "/"},
|
|
{input: "/", expected: "/"},
|
|
{input: "/foo", expected: "/foo"},
|
|
{input: "/foo/", expected: "/foo/"},
|
|
{input: "/../bar", wantErr: true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.input, func(t *testing.T) {
|
|
actual, err := cleanURLPath(tt.input)
|
|
|
|
if tt.wantErr == true && err == nil {
|
|
t.Errorf("Expected an error but got none")
|
|
return
|
|
}
|
|
|
|
if tt.wantErr == false && err != nil {
|
|
t.Errorf("Got an error, but didn't expect one: %v", err)
|
|
return
|
|
}
|
|
|
|
if actual != tt.expected {
|
|
t.Errorf("Got: %q; expected: %q", actual, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAddServiceToPrefs(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
svcName tailcfg.ServiceName
|
|
startServices []string
|
|
expected []string
|
|
}{
|
|
{
|
|
name: "add service to empty prefs",
|
|
svcName: "svc:foo",
|
|
expected: []string{"svc:foo"},
|
|
},
|
|
{
|
|
name: "add service to existing prefs",
|
|
svcName: "svc:bar",
|
|
startServices: []string{"svc:foo"},
|
|
expected: []string{"svc:foo", "svc:bar"},
|
|
},
|
|
{
|
|
name: "add existing service to prefs",
|
|
svcName: "svc:foo",
|
|
startServices: []string{"svc:foo"},
|
|
expected: []string{"svc:foo"},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
lc := &fakeLocalServeClient{}
|
|
ctx := t.Context()
|
|
lc.EditPrefs(ctx, &ipn.MaskedPrefs{
|
|
AdvertiseServicesSet: true,
|
|
Prefs: ipn.Prefs{
|
|
AdvertiseServices: tt.startServices,
|
|
},
|
|
})
|
|
e := &serveEnv{lc: lc, bg: bgBoolFlag{true, false}}
|
|
err := e.addServiceToPrefs(ctx, tt.svcName)
|
|
if err != nil {
|
|
t.Fatalf("addServiceToPrefs(%q) returned unexpected error: %v", tt.svcName, err)
|
|
}
|
|
if !slices.Equal(lc.prefs.AdvertiseServices, tt.expected) {
|
|
t.Errorf("addServiceToPrefs(%q) = %v, want %v", tt.svcName, lc.prefs.AdvertiseServices, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
func TestRemoveServiceFromPrefs(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
svcName tailcfg.ServiceName
|
|
startServices []string
|
|
expected []string
|
|
}{
|
|
{
|
|
name: "remove service from empty prefs",
|
|
svcName: "svc:foo",
|
|
expected: []string{},
|
|
},
|
|
{
|
|
name: "remove existing service from prefs",
|
|
svcName: "svc:foo",
|
|
startServices: []string{"svc:foo"},
|
|
expected: []string{},
|
|
},
|
|
{
|
|
name: "remove service not in prefs",
|
|
svcName: "svc:bar",
|
|
startServices: []string{"svc:foo"},
|
|
expected: []string{"svc:foo"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
lc := &fakeLocalServeClient{}
|
|
ctx := t.Context()
|
|
lc.EditPrefs(ctx, &ipn.MaskedPrefs{
|
|
AdvertiseServicesSet: true,
|
|
Prefs: ipn.Prefs{
|
|
AdvertiseServices: tt.startServices,
|
|
},
|
|
})
|
|
e := &serveEnv{lc: lc, bg: bgBoolFlag{true, false}}
|
|
err := e.removeServiceFromPrefs(ctx, tt.svcName)
|
|
if err != nil {
|
|
t.Fatalf("removeServiceFromPrefs(%q) returned unexpected error: %v", tt.svcName, err)
|
|
}
|
|
if !slices.Equal(lc.prefs.AdvertiseServices, tt.expected) {
|
|
t.Errorf("removeServiceFromPrefs(%q) = %v, want %v", tt.svcName, lc.prefs.AdvertiseServices, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMessageForPort(t *testing.T) {
|
|
svcIPMap := tailcfg.ServiceIPMappings{
|
|
"svc:foo": []netip.Addr{
|
|
netip.MustParseAddr("100.101.101.101"),
|
|
netip.MustParseAddr("fd7a:115c:a1e0:ab12:4843:cd96:6565:6565"),
|
|
},
|
|
}
|
|
svcIPMapJSON, _ := json.Marshal(svcIPMap)
|
|
svcIPMapJSONRawMSG := tailcfg.RawMessage(svcIPMapJSON)
|
|
|
|
tests := []struct {
|
|
name string
|
|
subcmd serveMode
|
|
serveConfig *ipn.ServeConfig
|
|
status *ipnstate.Status
|
|
prefs *ipn.Prefs
|
|
dnsName string
|
|
srvType serveType
|
|
srvPort uint16
|
|
expected string
|
|
}{
|
|
{
|
|
name: "funnel-https",
|
|
subcmd: funnel,
|
|
serveConfig: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
|
443: {HTTPS: true},
|
|
},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:443": {
|
|
Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://127.0.0.1:3000"},
|
|
},
|
|
},
|
|
},
|
|
AllowFunnel: map[ipn.HostPort]bool{
|
|
"foo.test.ts.net:443": true,
|
|
},
|
|
},
|
|
status: &ipnstate.Status{CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"}},
|
|
dnsName: "foo.test.ts.net",
|
|
srvType: serveTypeHTTPS,
|
|
srvPort: 443,
|
|
expected: strings.Join([]string{
|
|
msgFunnelAvailable,
|
|
"",
|
|
"https://foo.test.ts.net/",
|
|
"|-- proxy http://127.0.0.1:3000",
|
|
"",
|
|
fmt.Sprintf(msgRunningInBackground, "Funnel"),
|
|
fmt.Sprintf(msgDisableProxy, "funnel", "https", 443),
|
|
}, "\n"),
|
|
},
|
|
{
|
|
name: "serve-http",
|
|
subcmd: serve,
|
|
serveConfig: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
|
443: {HTTP: true},
|
|
},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:80": {
|
|
Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://127.0.0.1:3000"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
status: &ipnstate.Status{CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"}},
|
|
dnsName: "foo.test.ts.net",
|
|
srvType: serveTypeHTTP,
|
|
srvPort: 80,
|
|
expected: strings.Join([]string{
|
|
msgServeAvailable,
|
|
"",
|
|
"https://foo.test.ts.net:80/",
|
|
"|-- proxy http://127.0.0.1:3000",
|
|
"",
|
|
fmt.Sprintf(msgRunningInBackground, "Serve"),
|
|
fmt.Sprintf(msgDisableProxy, "serve", "http", 80),
|
|
}, "\n"),
|
|
},
|
|
{
|
|
name: "serve service http",
|
|
subcmd: serve,
|
|
serveConfig: &ipn.ServeConfig{
|
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
|
"svc:foo": {
|
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
|
80: {HTTP: true},
|
|
},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:80": {
|
|
Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://localhost:3000"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
status: &ipnstate.Status{
|
|
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
|
|
Self: &ipnstate.PeerStatus{
|
|
CapMap: tailcfg.NodeCapMap{
|
|
tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{svcIPMapJSONRawMSG},
|
|
},
|
|
},
|
|
},
|
|
prefs: &ipn.Prefs{
|
|
AdvertiseServices: []string{"svc:foo"},
|
|
},
|
|
dnsName: "svc:foo",
|
|
srvType: serveTypeHTTP,
|
|
srvPort: 80,
|
|
expected: strings.Join([]string{
|
|
msgServeAvailable,
|
|
"",
|
|
"http://foo.test.ts.net/",
|
|
"|-- proxy http://localhost:3000",
|
|
"",
|
|
fmt.Sprintf(msgRunningInBackground, "Serve"),
|
|
fmt.Sprintf(msgDisableServiceProxy, "svc:foo", "http", 80),
|
|
fmt.Sprintf(msgDisableService, "svc:foo"),
|
|
}, "\n"),
|
|
},
|
|
{
|
|
name: "serve service no capmap",
|
|
subcmd: serve,
|
|
serveConfig: &ipn.ServeConfig{
|
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
|
"svc:bar": {
|
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
|
80: {HTTP: true},
|
|
},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"bar.test.ts.net:80": {
|
|
Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://localhost:3000"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
status: &ipnstate.Status{
|
|
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
|
|
Self: &ipnstate.PeerStatus{
|
|
CapMap: tailcfg.NodeCapMap{
|
|
tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{svcIPMapJSONRawMSG},
|
|
},
|
|
},
|
|
},
|
|
prefs: &ipn.Prefs{
|
|
AdvertiseServices: []string{"svc:bar"},
|
|
},
|
|
dnsName: "svc:bar",
|
|
srvType: serveTypeHTTP,
|
|
srvPort: 80,
|
|
expected: strings.Join([]string{
|
|
fmt.Sprintf(msgServiceWaitingApproval, "svc:bar"),
|
|
"",
|
|
"http://bar.test.ts.net/",
|
|
"|-- proxy http://localhost:3000",
|
|
"",
|
|
fmt.Sprintf(msgRunningInBackground, "Serve"),
|
|
fmt.Sprintf(msgDisableServiceProxy, "svc:bar", "http", 80),
|
|
fmt.Sprintf(msgDisableService, "svc:bar"),
|
|
}, "\n"),
|
|
},
|
|
{
|
|
name: "serve service https non-default port",
|
|
subcmd: serve,
|
|
serveConfig: &ipn.ServeConfig{
|
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
|
"svc:foo": {
|
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
|
2200: {HTTPS: true},
|
|
},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:2200": {
|
|
Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://localhost:3000"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
status: &ipnstate.Status{
|
|
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
|
|
Self: &ipnstate.PeerStatus{
|
|
CapMap: tailcfg.NodeCapMap{
|
|
tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{svcIPMapJSONRawMSG},
|
|
},
|
|
},
|
|
},
|
|
prefs: &ipn.Prefs{AdvertiseServices: []string{"svc:foo"}},
|
|
dnsName: "svc:foo",
|
|
srvType: serveTypeHTTPS,
|
|
srvPort: 2200,
|
|
expected: strings.Join([]string{
|
|
msgServeAvailable,
|
|
"",
|
|
"https://foo.test.ts.net:2200/",
|
|
"|-- proxy http://localhost:3000",
|
|
"",
|
|
fmt.Sprintf(msgRunningInBackground, "Serve"),
|
|
fmt.Sprintf(msgDisableServiceProxy, "svc:foo", "https", 2200),
|
|
fmt.Sprintf(msgDisableService, "svc:foo"),
|
|
}, "\n"),
|
|
},
|
|
{
|
|
name: "serve service TCPForward",
|
|
subcmd: serve,
|
|
serveConfig: &ipn.ServeConfig{
|
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
|
"svc:foo": {
|
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
|
2200: {TCPForward: "localhost:3000"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
status: &ipnstate.Status{
|
|
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
|
|
Self: &ipnstate.PeerStatus{
|
|
CapMap: tailcfg.NodeCapMap{
|
|
tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{svcIPMapJSONRawMSG},
|
|
},
|
|
},
|
|
},
|
|
prefs: &ipn.Prefs{AdvertiseServices: []string{"svc:foo"}},
|
|
dnsName: "svc:foo",
|
|
srvType: serveTypeTCP,
|
|
srvPort: 2200,
|
|
expected: strings.Join([]string{
|
|
msgServeAvailable,
|
|
"",
|
|
"|-- tcp://foo.test.ts.net:2200 (TLS over TCP)",
|
|
"|-- tcp://100.101.101.101:2200",
|
|
"|-- tcp://[fd7a:115c:a1e0:ab12:4843:cd96:6565:6565]:2200",
|
|
"|--> tcp://localhost:3000",
|
|
"",
|
|
fmt.Sprintf(msgRunningInBackground, "Serve"),
|
|
fmt.Sprintf(msgDisableServiceProxy, "svc:foo", "tcp", 2200),
|
|
fmt.Sprintf(msgDisableService, "svc:foo"),
|
|
}, "\n"),
|
|
},
|
|
{
|
|
name: "serve service Tun",
|
|
subcmd: serve,
|
|
serveConfig: &ipn.ServeConfig{
|
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
|
"svc:foo": {
|
|
Tun: true,
|
|
},
|
|
},
|
|
},
|
|
status: &ipnstate.Status{
|
|
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
|
|
Self: &ipnstate.PeerStatus{
|
|
CapMap: tailcfg.NodeCapMap{
|
|
tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{svcIPMapJSONRawMSG},
|
|
},
|
|
},
|
|
},
|
|
prefs: &ipn.Prefs{AdvertiseServices: []string{"svc:foo"}},
|
|
dnsName: "svc:foo",
|
|
srvType: serveTypeTUN,
|
|
expected: strings.Join([]string{
|
|
msgServeAvailable,
|
|
"",
|
|
fmt.Sprintf(msgRunningTunService, "foo.test.ts.net"),
|
|
fmt.Sprintf(msgDisableServiceTun, "svc:foo"),
|
|
fmt.Sprintf(msgDisableService, "svc:foo"),
|
|
}, "\n"),
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
e := &serveEnv{bg: bgBoolFlag{true, false}, subcmd: tt.subcmd}
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
actual := e.messageForPort(tt.serveConfig, tt.status, tt.dnsName, tt.srvType, tt.srvPort)
|
|
|
|
if actual == "" {
|
|
t.Errorf("Got empty message")
|
|
}
|
|
|
|
if actual != tt.expected {
|
|
t.Errorf("\nGot: %q\nExpected: %q", actual, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsLegacyInvocation(t *testing.T) {
|
|
tests := []struct {
|
|
subcmd serveMode
|
|
args []string
|
|
expected bool
|
|
translation string
|
|
}{
|
|
{
|
|
subcmd: serve,
|
|
args: []string{"https", "/", "localhost:3000"},
|
|
expected: true,
|
|
translation: "tailscale serve --bg localhost:3000",
|
|
},
|
|
{
|
|
subcmd: serve,
|
|
args: []string{"https", "/", "localhost:3000", "off"},
|
|
expected: true,
|
|
translation: "tailscale serve --bg localhost:3000 off",
|
|
},
|
|
{
|
|
subcmd: serve,
|
|
args: []string{"https", "/", "off"},
|
|
expected: true,
|
|
translation: "tailscale serve --bg off",
|
|
},
|
|
{
|
|
subcmd: serve,
|
|
args: []string{"https:4545", "/foo", "off"},
|
|
expected: true,
|
|
translation: "tailscale serve --bg --https 4545 --set-path /foo off",
|
|
},
|
|
{
|
|
subcmd: serve,
|
|
args: []string{"https:4545", "/foo", "localhost:9090", "off"},
|
|
expected: true,
|
|
translation: "tailscale serve --bg --https 4545 --set-path /foo localhost:9090 off",
|
|
},
|
|
{
|
|
subcmd: serve,
|
|
args: []string{"https:8443", "/", "localhost:3000"},
|
|
expected: true,
|
|
translation: "tailscale serve --bg --https 8443 localhost:3000",
|
|
},
|
|
{
|
|
subcmd: serve,
|
|
args: []string{"http", "/", "localhost:3000"},
|
|
expected: true,
|
|
translation: "tailscale serve --bg --http 80 localhost:3000",
|
|
},
|
|
{
|
|
subcmd: serve,
|
|
args: []string{"http:80", "/", "localhost:3000"},
|
|
expected: true,
|
|
translation: "tailscale serve --bg --http 80 localhost:3000",
|
|
},
|
|
{
|
|
subcmd: serve,
|
|
args: []string{"https:10000", "/motd.txt", `text:Hello, world!`},
|
|
expected: true,
|
|
translation: `tailscale serve --bg --https 10000 --set-path /motd.txt "text:Hello, world!"`,
|
|
},
|
|
{
|
|
subcmd: serve,
|
|
args: []string{"tcp:2222", "tcp://localhost:22"},
|
|
expected: true,
|
|
translation: "tailscale serve --bg --tcp 2222 tcp://localhost:22",
|
|
},
|
|
{
|
|
subcmd: serve,
|
|
args: []string{"tls-terminated-tcp:443", "tcp://localhost:80"},
|
|
expected: true,
|
|
translation: "tailscale serve --bg --tls-terminated-tcp 443 tcp://localhost:80",
|
|
},
|
|
{
|
|
subcmd: funnel,
|
|
args: []string{"443", "on"},
|
|
expected: true,
|
|
},
|
|
{
|
|
subcmd: funnel,
|
|
args: []string{"443", "off"},
|
|
expected: true,
|
|
},
|
|
|
|
{
|
|
subcmd: serve,
|
|
args: []string{"3000"},
|
|
expected: false,
|
|
},
|
|
{
|
|
subcmd: serve,
|
|
args: []string{"localhost:3000"},
|
|
expected: false,
|
|
},
|
|
}
|
|
|
|
for idx, tt := range tests {
|
|
t.Run(strconv.Itoa(idx), func(t *testing.T) {
|
|
gotTranslation, actual := isLegacyInvocation(tt.subcmd, tt.args)
|
|
|
|
if actual != tt.expected {
|
|
t.Fatalf("got: %v; expected: %v", actual, tt.expected)
|
|
}
|
|
|
|
if gotTranslation != tt.translation {
|
|
t.Fatalf("expected translation to be %q but got %q", tt.translation, gotTranslation)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSetServe(t *testing.T) {
|
|
e := &serveEnv{}
|
|
magicDNSSuffix := "test.ts.net"
|
|
tests := []struct {
|
|
name string
|
|
desc string
|
|
cfg *ipn.ServeConfig
|
|
st *ipnstate.Status
|
|
dnsName string
|
|
srvType serveType
|
|
srvPort uint16
|
|
mountPath string
|
|
target string
|
|
allowFunnel bool
|
|
proxyProtocol int
|
|
expected *ipn.ServeConfig
|
|
expectErr bool
|
|
}{
|
|
{
|
|
name: "add new handler",
|
|
desc: "add a new http handler to empty config",
|
|
cfg: &ipn.ServeConfig{},
|
|
dnsName: "foo.test.ts.net",
|
|
srvType: serveTypeHTTP,
|
|
srvPort: 80,
|
|
mountPath: "/",
|
|
target: "http://localhost:3000",
|
|
expected: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:80": {
|
|
Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://localhost:3000"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "update http handler",
|
|
desc: "update an existing http handler on the same port to same type",
|
|
cfg: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:80": {
|
|
Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://localhost:3000"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
dnsName: "foo.test.ts.net",
|
|
srvType: serveTypeHTTP,
|
|
srvPort: 80,
|
|
mountPath: "/",
|
|
target: "http://localhost:3001",
|
|
expected: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:80": {
|
|
Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://localhost:3001"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "update TCP handler",
|
|
desc: "update an existing TCP handler on the same port to a http handler",
|
|
cfg: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{80: {TCPForward: "http://localhost:3000"}},
|
|
},
|
|
dnsName: "foo.test.ts.net",
|
|
srvType: serveTypeHTTP,
|
|
srvPort: 80,
|
|
mountPath: "/",
|
|
target: "http://localhost:3001",
|
|
expectErr: true,
|
|
},
|
|
{
|
|
name: "add new service handler",
|
|
desc: "add a new service TCP handler to empty config",
|
|
cfg: &ipn.ServeConfig{},
|
|
|
|
dnsName: "svc:bar",
|
|
srvType: serveTypeTCP,
|
|
srvPort: 80,
|
|
target: "3000",
|
|
expected: &ipn.ServeConfig{
|
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
|
"svc:bar": {
|
|
TCP: map[uint16]*ipn.TCPPortHandler{80: {TCPForward: "127.0.0.1:3000"}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "update service handler",
|
|
desc: "update an existing service TCP handler on the same port to same type",
|
|
cfg: &ipn.ServeConfig{
|
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
|
"svc:bar": {
|
|
TCP: map[uint16]*ipn.TCPPortHandler{80: {TCPForward: "127.0.0.1:3000"}},
|
|
},
|
|
},
|
|
},
|
|
dnsName: "svc:bar",
|
|
srvType: serveTypeTCP,
|
|
srvPort: 80,
|
|
target: "3001",
|
|
expected: &ipn.ServeConfig{
|
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
|
"svc:bar": {
|
|
TCP: map[uint16]*ipn.TCPPortHandler{80: {TCPForward: "127.0.0.1:3001"}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "update service handler",
|
|
desc: "update an existing service TCP handler on the same port to a http handler",
|
|
cfg: &ipn.ServeConfig{
|
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
|
"svc:bar": {
|
|
TCP: map[uint16]*ipn.TCPPortHandler{80: {TCPForward: "127.0.0.1:3000"}},
|
|
},
|
|
},
|
|
},
|
|
dnsName: "svc:bar",
|
|
srvType: serveTypeHTTP,
|
|
srvPort: 80,
|
|
mountPath: "/",
|
|
target: "http://localhost:3001",
|
|
expectErr: true,
|
|
},
|
|
{
|
|
name: "add new service handler",
|
|
desc: "add a new service HTTP handler to empty config",
|
|
cfg: &ipn.ServeConfig{},
|
|
dnsName: "svc:bar",
|
|
srvType: serveTypeHTTP,
|
|
srvPort: 80,
|
|
mountPath: "/",
|
|
target: "http://localhost:3000",
|
|
expected: &ipn.ServeConfig{
|
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
|
"svc:bar": {
|
|
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"bar.test.ts.net:80": {
|
|
Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://localhost:3000"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "update existing service handler",
|
|
desc: "update an existing service HTTP handler",
|
|
cfg: &ipn.ServeConfig{
|
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
|
"svc:bar": {
|
|
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"bar.test.ts.net:80": {
|
|
Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://localhost:3000"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
dnsName: "svc:bar",
|
|
srvType: serveTypeHTTP,
|
|
srvPort: 80,
|
|
mountPath: "/",
|
|
target: "http://localhost:3001",
|
|
expected: &ipn.ServeConfig{
|
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
|
"svc:bar": {
|
|
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"bar.test.ts.net:80": {
|
|
Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://localhost:3001"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "add new service handler",
|
|
desc: "add a new service HTTP handler to existing service config",
|
|
cfg: &ipn.ServeConfig{
|
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
|
"svc:bar": {
|
|
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"bar.test.ts.net:80": {
|
|
Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://localhost:3000"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
dnsName: "svc:bar",
|
|
srvType: serveTypeHTTP,
|
|
srvPort: 88,
|
|
mountPath: "/",
|
|
target: "http://localhost:3001",
|
|
expected: &ipn.ServeConfig{
|
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
|
"svc:bar": {
|
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
|
80: {HTTP: true},
|
|
88: {HTTP: true},
|
|
},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"bar.test.ts.net:80": {
|
|
Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://localhost:3000"},
|
|
},
|
|
},
|
|
"bar.test.ts.net:88": {
|
|
Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://localhost:3001"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "add new service mount",
|
|
desc: "add a new service mount to existing service config",
|
|
cfg: &ipn.ServeConfig{
|
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
|
"svc:bar": {
|
|
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"bar.test.ts.net:80": {
|
|
Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://localhost:3000"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
dnsName: "svc:bar",
|
|
srvType: serveTypeHTTP,
|
|
srvPort: 80,
|
|
mountPath: "/added",
|
|
target: "http://localhost:3001",
|
|
expected: &ipn.ServeConfig{
|
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
|
"svc:bar": {
|
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
|
80: {HTTP: true},
|
|
},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"bar.test.ts.net:80": {
|
|
Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://localhost:3000"},
|
|
"/added": {Proxy: "http://localhost:3001"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "add new service handler",
|
|
desc: "add a new service handler in tun mode to empty config",
|
|
cfg: &ipn.ServeConfig{},
|
|
dnsName: "svc:bar",
|
|
srvType: serveTypeTUN,
|
|
expected: &ipn.ServeConfig{
|
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
|
"svc:bar": {
|
|
Tun: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := e.setServe(tt.cfg, tt.dnsName, tt.srvType, tt.srvPort, tt.mountPath, tt.target, tt.allowFunnel, magicDNSSuffix, nil, tt.proxyProtocol)
|
|
if err != nil && !tt.expectErr {
|
|
t.Fatalf("got error: %v; did not expect error.", err)
|
|
}
|
|
if err == nil && tt.expectErr {
|
|
t.Fatalf("got no error; expected error.")
|
|
}
|
|
if !tt.expectErr {
|
|
if diff := cmp.Diff(tt.expected, tt.cfg); diff != "" {
|
|
// svcName := tailcfg.ServiceName(tt.dnsName)
|
|
t.Fatalf("got diff:\n%s", diff)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUnsetServe(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
desc string
|
|
cfg *ipn.ServeConfig
|
|
st *ipnstate.Status
|
|
dnsName string
|
|
srvType serveType
|
|
srvPort uint16
|
|
mount string
|
|
setServeEnv bool
|
|
serveEnv *serveEnv // if set, use this instead of the default serveEnv
|
|
expected *ipn.ServeConfig
|
|
expectErr bool
|
|
}{
|
|
{
|
|
name: "unset http handler",
|
|
desc: "remove an existing http handler",
|
|
cfg: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
|
80: {HTTP: true},
|
|
},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo.test.ts.net:80": {
|
|
Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://localhost:3000"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
st: &ipnstate.Status{
|
|
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
|
|
},
|
|
dnsName: "foo.test.ts.net",
|
|
srvType: serveTypeHTTP,
|
|
srvPort: 80,
|
|
mount: "/",
|
|
expected: &ipn.ServeConfig{},
|
|
expectErr: false,
|
|
},
|
|
{
|
|
name: "unset service handler",
|
|
desc: "remove an existing service TCP handler",
|
|
cfg: &ipn.ServeConfig{
|
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
|
"svc:bar": {
|
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
|
80: {HTTP: true},
|
|
},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"bar.test.ts.net:80": {
|
|
Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://localhost:3000"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
st: &ipnstate.Status{
|
|
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
|
|
},
|
|
dnsName: "svc:bar",
|
|
srvType: serveTypeHTTP,
|
|
srvPort: 80,
|
|
mount: "/",
|
|
expected: &ipn.ServeConfig{},
|
|
expectErr: false,
|
|
},
|
|
{
|
|
name: "unset service handler tun",
|
|
desc: "remove an existing service handler in tun mode",
|
|
cfg: &ipn.ServeConfig{
|
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
|
"svc:bar": {
|
|
Tun: true,
|
|
},
|
|
},
|
|
},
|
|
st: &ipnstate.Status{
|
|
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
|
|
},
|
|
dnsName: "svc:bar",
|
|
srvType: serveTypeTUN,
|
|
expected: &ipn.ServeConfig{},
|
|
expectErr: false,
|
|
},
|
|
{
|
|
name: "unset service handler tcp",
|
|
desc: "remove an existing service TCP handler",
|
|
cfg: &ipn.ServeConfig{
|
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
|
"svc:bar": {
|
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
|
80: {TCPForward: "11.11.11.11:3000"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
st: &ipnstate.Status{
|
|
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
|
|
},
|
|
dnsName: "svc:bar",
|
|
srvType: serveTypeTCP,
|
|
srvPort: 80,
|
|
expected: &ipn.ServeConfig{},
|
|
expectErr: false,
|
|
},
|
|
{
|
|
name: "unset http handler not found",
|
|
desc: "try to remove a non-existing http handler",
|
|
cfg: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
|
80: {HTTP: true},
|
|
},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo:80": {
|
|
Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://localhost:3000"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
st: &ipnstate.Status{
|
|
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
|
|
},
|
|
dnsName: "bar.test.ts.net",
|
|
srvType: serveTypeHTTP,
|
|
srvPort: 80,
|
|
mount: "/abc",
|
|
expected: &ipn.ServeConfig{},
|
|
expectErr: true,
|
|
},
|
|
{
|
|
name: "unset service handler not found",
|
|
desc: "try to remove a non-existing service TCP handler",
|
|
|
|
cfg: &ipn.ServeConfig{
|
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
|
"svc:bar": {
|
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
|
80: {HTTP: true},
|
|
},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"bar.test.ts.net:80": {
|
|
Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://localhost:3000"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
st: &ipnstate.Status{
|
|
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
|
|
},
|
|
dnsName: "svc:bar",
|
|
srvType: serveTypeHTTP,
|
|
srvPort: 80,
|
|
mount: "/abc",
|
|
setServeEnv: true,
|
|
serveEnv: &serveEnv{setPath: "/abc"},
|
|
expected: &ipn.ServeConfig{},
|
|
expectErr: true,
|
|
},
|
|
{
|
|
name: "unset service doesn't exist",
|
|
desc: "try to remove a non-existing service's handler",
|
|
cfg: &ipn.ServeConfig{
|
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
|
"svc:bar": {
|
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
|
80: {TCPForward: "11.11.11.11:3000"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
st: &ipnstate.Status{
|
|
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
|
|
},
|
|
dnsName: "svc:foo",
|
|
srvType: serveTypeTCP,
|
|
srvPort: 80,
|
|
expectErr: true,
|
|
},
|
|
{
|
|
name: "unset tcp while port is in use",
|
|
desc: "try to remove a TCP handler while the port is used for web",
|
|
cfg: &ipn.ServeConfig{
|
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
|
80: {HTTP: true},
|
|
},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"foo:80": {
|
|
Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://localhost:3000"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
st: &ipnstate.Status{
|
|
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
|
|
},
|
|
dnsName: "foo.test.ts.net",
|
|
srvType: serveTypeTCP,
|
|
srvPort: 80,
|
|
mount: "/",
|
|
expectErr: true,
|
|
},
|
|
{
|
|
name: "unset service tcp while port is in use",
|
|
desc: "try to remove a service TCP handler while the port is used for web",
|
|
cfg: &ipn.ServeConfig{
|
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
|
"svc:bar": {
|
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
|
80: {HTTP: true},
|
|
},
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"bar.test.ts.net:80": {
|
|
Handlers: map[string]*ipn.HTTPHandler{
|
|
"/": {Proxy: "http://localhost:3000"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
st: &ipnstate.Status{
|
|
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
|
|
},
|
|
dnsName: "svc:bar",
|
|
srvType: serveTypeTCP,
|
|
srvPort: 80,
|
|
mount: "/",
|
|
expectErr: true,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
e := &serveEnv{}
|
|
if tt.setServeEnv {
|
|
e = tt.serveEnv
|
|
}
|
|
err := e.unsetServe(tt.cfg, tt.dnsName, tt.srvType, tt.srvPort, tt.mount, tt.st.CurrentTailnet.MagicDNSSuffix)
|
|
if err != nil && !tt.expectErr {
|
|
t.Fatalf("got error: %v; did not expect error.", err)
|
|
}
|
|
if err == nil && tt.expectErr {
|
|
t.Fatalf("got no error; expected error.")
|
|
}
|
|
if !tt.expectErr && !reflect.DeepEqual(tt.cfg, tt.expected) {
|
|
t.Fatalf("got: %v; expected: %v", tt.cfg, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// exactErrMsg returns an error checker that wants exactly the provided want error.
|
|
// If optName is non-empty, it's used in the error message.
|
|
func exactErrMsg(want error) func(error) string {
|
|
return func(got error) string {
|
|
if got.Error() == want.Error() {
|
|
return ""
|
|
}
|
|
return fmt.Sprintf("\ngot: %v\nwant: %v\n", got, want)
|
|
}
|
|
}
|
|
|
|
func ptrToReadOnlySlice[T any](s []T) *views.Slice[T] {
|
|
vs := views.SliceOf(s)
|
|
return &vs
|
|
}
|