mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-14 18:59:08 +02:00
The endpoint-comparison rules this branch introduced now live in util (PR #7472 moved them there so the MDM conflict check could stop comparing URLs as strings). Keeping a copy here is what produced that bug in the first place: two implementations of "is this the same endpoint?" drift, and the one that drifts starts refusing a URL that addresses the very server it already points at. So SameServiceURL delegates the port normalization to util.ServiceURLPort and drops the local one, and SameServiceURLIncludingPath — endpoint plus path, for the admin panel URL, which is opened rather than dialed — is util.SameServiceURL plus the query, fragment and userinfo it adds on top, so the local path normalization goes too. What stays here is the distinction util does not make: SameServiceURL is endpoint-only, because a management URL is dialed and only its host and port are, while util.SameServiceURL includes the path. Pure refactor. Verified as one: all 198 pairs of a 14-spelling matrix (default and zero-padded ports, host case, trailing slash, path, query, fragment, userinfo, both schemes, nil operands) answer identically for both functions before and after.
495 lines
19 KiB
Go
495 lines
19 KiB
Go
package profilemanager
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/netbirdio/netbird/client/iface"
|
|
"github.com/netbirdio/netbird/shared/management/domain"
|
|
)
|
|
|
|
func seededConfig(t *testing.T) *Config {
|
|
t.Helper()
|
|
|
|
path := filepath.Join(t.TempDir(), "seeded.json")
|
|
cfg, err := UpdateOrCreateConfig(ConfigInput{
|
|
ConfigPath: path,
|
|
ManagementURL: "https://api.netbird.io:443",
|
|
PreSharedKey: strPointer("stored-key"),
|
|
})
|
|
require.NoError(t, err)
|
|
return cfg
|
|
}
|
|
|
|
func strPointer(s string) *string { return &s }
|
|
|
|
func intPtr(i int) *int { return &i }
|
|
|
|
func TestWouldChange(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input ConfigInput
|
|
want bool
|
|
}{
|
|
{name: "empty input", input: ConfigInput{}, want: false},
|
|
{name: "same management URL", input: ConfigInput{ManagementURL: "https://api.netbird.io:443"}, want: false},
|
|
{name: "management URL without its default port", input: ConfigInput{ManagementURL: "https://api.netbird.io"}, want: false},
|
|
{name: "different management URL", input: ConfigInput{ManagementURL: "https://other.example:443"}, want: true},
|
|
{name: "same pre-shared key", input: ConfigInput{PreSharedKey: strPointer("stored-key")}, want: false},
|
|
{name: "redacted pre-shared key", input: ConfigInput{PreSharedKey: strPointer("**********")}, want: false},
|
|
{name: "different pre-shared key", input: ConfigInput{PreSharedKey: strPointer("other-key")}, want: true},
|
|
{name: "new interface blacklist entry", input: ConfigInput{ExtraIFaceBlackList: []string{"nb-probe0"}}, want: true},
|
|
{name: "blacklist entry already present", input: ConfigInput{ExtraIFaceBlackList: []string{"lo"}}, want: false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
cfg := seededConfig(t)
|
|
|
|
changed, err := cfg.WouldChange(tt.input)
|
|
require.NoError(t, err)
|
|
require.Equal(t, tt.want, changed)
|
|
})
|
|
}
|
|
}
|
|
|
|
// The dry run must not be observable on the config it is run against: it
|
|
// decides whether a write is allowed, it does not perform one.
|
|
func TestWouldChangeLeavesTheConfigAlone(t *testing.T) {
|
|
cfg := seededConfig(t)
|
|
blacklist := len(cfg.IFaceBlackList)
|
|
|
|
changed, err := cfg.WouldChange(ConfigInput{
|
|
ManagementURL: "https://other.example:443",
|
|
PreSharedKey: strPointer("other-key"),
|
|
ExtraIFaceBlackList: []string{"nb-probe0"},
|
|
DNSLabels: domain.FromPunycodeList([]string{"probe"}),
|
|
NATExternalIPs: []string{"1.2.3.4"},
|
|
})
|
|
require.NoError(t, err)
|
|
require.True(t, changed)
|
|
|
|
require.Equal(t, "https://api.netbird.io:443", cfg.ManagementURL.String())
|
|
require.Equal(t, "stored-key", cfg.PreSharedKey)
|
|
require.Len(t, cfg.IFaceBlackList, blacklist)
|
|
require.Empty(t, cfg.DNSLabels)
|
|
require.Empty(t, cfg.NATExternalIPs)
|
|
}
|
|
|
|
// A nil config means the profile holds nothing yet, so the baseline is what
|
|
// the daemon would create for it.
|
|
func TestWouldChangeWithoutAStoredConfig(t *testing.T) {
|
|
var cfg *Config
|
|
|
|
changed, err := cfg.WouldChange(ConfigInput{})
|
|
require.NoError(t, err)
|
|
require.False(t, changed, "a request carrying nothing cannot change anything")
|
|
|
|
changed, err = cfg.WouldChange(ConfigInput{ManagementURL: DefaultManagementURL})
|
|
require.NoError(t, err)
|
|
require.False(t, changed, "the default management URL is what would be written anyway")
|
|
|
|
changed, err = cfg.WouldChange(ConfigInput{ManagementURL: "https://other.example:443"})
|
|
require.NoError(t, err)
|
|
require.True(t, changed)
|
|
}
|
|
|
|
func TestWouldChangeReportsAnInvalidInput(t *testing.T) {
|
|
cfg := seededConfig(t)
|
|
|
|
_, err := cfg.WouldChange(ConfigInput{ManagementURL: "not-a-url"})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
// Reads must not write. A config file missing a field apply() fills in (MTU,
|
|
// here) is what used to trigger the write-back.
|
|
func TestReadsDoNotWriteTheConfigBack(t *testing.T) {
|
|
denormalized := []byte(`{"WgIface":"wt0"}`)
|
|
|
|
for name, read := range map[string]func(string) (*Config, error){
|
|
"GetExistingConfig": GetExistingConfig,
|
|
"ReadOrGenerateConfig": ReadOrGenerateConfig,
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "profile.json")
|
|
require.NoError(t, os.WriteFile(path, denormalized, 0o600))
|
|
|
|
cfg, err := read(path)
|
|
require.NoError(t, err)
|
|
require.Equal(t, uint16(iface.DefaultMTU), cfg.MTU, "the returned config is still normalized in memory")
|
|
require.Empty(t, cfg.PrivateKey, "a read must not mint an identity either")
|
|
|
|
after, err := os.ReadFile(path)
|
|
require.NoError(t, err)
|
|
require.Equal(t, string(denormalized), string(after), "%s rewrote the config file", name)
|
|
})
|
|
}
|
|
}
|
|
|
|
// ReadConfig resolves a default config for a profile that has no file yet, and
|
|
// that must not create the file either.
|
|
func TestReadConfigDoesNotCreateTheFile(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "absent.json")
|
|
|
|
cfg, err := ReadOrGenerateConfig(path)
|
|
require.NoError(t, err)
|
|
require.Equal(t, DefaultManagementURL, cfg.ManagementURL.String())
|
|
|
|
_, err = os.Stat(path)
|
|
require.True(t, os.IsNotExist(err), "ReadConfig created the config file")
|
|
}
|
|
|
|
// The identity is the one thing a read cannot recompute, so it is provisioned
|
|
// on request and its caller persists it.
|
|
func TestEnsureIdentity(t *testing.T) {
|
|
cfg := newConfigSkeleton()
|
|
|
|
generated, err := cfg.EnsureIdentity()
|
|
require.NoError(t, err)
|
|
require.True(t, generated)
|
|
require.NotEmpty(t, cfg.PrivateKey)
|
|
require.NotEmpty(t, cfg.SSHKey)
|
|
|
|
key := cfg.PrivateKey
|
|
generated, err = cfg.EnsureIdentity()
|
|
require.NoError(t, err)
|
|
require.False(t, generated, "a config that already has an identity keeps it")
|
|
require.Equal(t, key, cfg.PrivateKey)
|
|
}
|
|
|
|
// One endpoint written several ways is one endpoint. A gate that compared
|
|
// spellings refused a client restating its own management URL with a trailing
|
|
// slash, which is a normal way to write it.
|
|
func TestSameServiceURL(t *testing.T) {
|
|
tests := []struct {
|
|
a, b string
|
|
want bool
|
|
}{
|
|
{a: "https://mgmt.example.com", b: "https://mgmt.example.com:443", want: true},
|
|
{a: "https://mgmt.example.com", b: "https://mgmt.example.com/", want: true},
|
|
{a: "https://mgmt.example.com/", b: "https://mgmt.example.com:443/", want: true},
|
|
{a: "https://MGMT.example.com", b: "https://mgmt.example.com", want: true},
|
|
{a: "http://mgmt.example.com", b: "http://mgmt.example.com:80", want: true},
|
|
{a: "https://mgmt.example.com", b: "http://mgmt.example.com", want: false},
|
|
{a: "https://mgmt.example.com", b: "https://mgmt.example.com:8443", want: false},
|
|
{a: "https://mgmt.example.com", b: "https://other.example.com", want: false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.a+" vs "+tt.b, func(t *testing.T) {
|
|
a, err := ParseServiceURL("a", tt.a)
|
|
require.NoError(t, err)
|
|
b, err := ParseServiceURL("b", tt.b)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, tt.want, SameServiceURL(a, b))
|
|
require.Equal(t, tt.want, SameServiceURL(b, a), "the comparison must be symmetric")
|
|
})
|
|
}
|
|
}
|
|
|
|
// The same spellings, through the dry run the update-settings gate uses.
|
|
func TestWouldChangeIgnoresURLSpelling(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "seeded.json")
|
|
_, err := UpdateOrCreateConfig(ConfigInput{
|
|
ConfigPath: path,
|
|
ManagementURL: "https://mgmt.example.com",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
cfg, err := GetExistingConfig(path)
|
|
require.NoError(t, err)
|
|
|
|
for _, spelling := range []string{
|
|
"https://mgmt.example.com",
|
|
"https://mgmt.example.com/",
|
|
"https://mgmt.example.com:443",
|
|
"https://mgmt.example.com:443/",
|
|
"https://MGMT.example.com",
|
|
} {
|
|
changed, err := cfg.WouldChange(ConfigInput{ManagementURL: spelling})
|
|
require.NoError(t, err)
|
|
require.False(t, changed, "%q is the stored endpoint written differently", spelling)
|
|
}
|
|
|
|
changed, err := cfg.WouldChange(ConfigInput{ManagementURL: "https://mgmt.example.com:8443"})
|
|
require.NoError(t, err)
|
|
require.True(t, changed, "a different port is a different endpoint")
|
|
}
|
|
|
|
// The dry-run baseline exists to be compared against and discarded, so it must
|
|
// not mint keys — the CLI's login backoff loop would otherwise log a fresh
|
|
// "generated new Wireguard key" on every attempt.
|
|
func TestDryRunBaselineDoesNotGenerateKeys(t *testing.T) {
|
|
baseline, err := newDryRunBaseline(filepath.Join(t.TempDir(), "absent.json"))
|
|
require.NoError(t, err)
|
|
|
|
require.Empty(t, baseline.PrivateKey, "generated a WireGuard key for a throwaway config")
|
|
require.Empty(t, baseline.SSHKey, "generated an SSH key for a throwaway config")
|
|
|
|
// Everything the comparison actually looks at is still the default config.
|
|
require.Equal(t, DefaultManagementURL, baseline.ManagementURL.String())
|
|
require.Equal(t, uint16(iface.DefaultMTU), baseline.MTU)
|
|
require.Equal(t, iface.DefaultWgPort, baseline.WgPort)
|
|
}
|
|
|
|
// A stored profile can carry no identity — a mobile logout clears the keys in
|
|
// place — so the next config write has to mint one, which is what keeps the
|
|
// following login from dialing management with an empty key.
|
|
func TestUpdateConfigProvisionsAMissingIdentity(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "logged-out.json")
|
|
_, err := UpdateOrCreateConfig(ConfigInput{
|
|
ConfigPath: path,
|
|
ManagementURL: "https://api.netbird.io:443",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Stand in for the logout, which zeroes the keys and writes the config out.
|
|
loggedOut, err := GetExistingConfig(path)
|
|
require.NoError(t, err)
|
|
loggedOut.PrivateKey = ""
|
|
loggedOut.SSHKey = ""
|
|
require.NoError(t, WriteOutConfig(path, loggedOut))
|
|
|
|
cfg, err := UpdateOrCreateConfig(ConfigInput{ConfigPath: path})
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, cfg.PrivateKey, "the write path did not provision an identity")
|
|
require.NotEmpty(t, cfg.SSHKey)
|
|
|
|
persisted, err := GetExistingConfig(path)
|
|
require.NoError(t, err)
|
|
require.Equal(t, cfg.PrivateKey, persisted.PrivateKey, "the provisioned identity was not persisted")
|
|
}
|
|
|
|
// A config that carries no sync message version must not make the dry run
|
|
// panic: the gate runs inside a request handler, where failing closed is the
|
|
// worst acceptable outcome.
|
|
func TestWouldChangeWithoutAStoredSyncMessageVersion(t *testing.T) {
|
|
cfg := seededConfig(t)
|
|
require.Nil(t, cfg.SyncMessageVersion, "the fixture is only useful while the field starts out unset")
|
|
|
|
version := 2
|
|
changed, err := cfg.WouldChange(ConfigInput{SyncMessageVersion: &version})
|
|
require.NoError(t, err)
|
|
require.True(t, changed)
|
|
require.Nil(t, cfg.SyncMessageVersion, "the dry run set the version on the stored config")
|
|
}
|
|
|
|
// Restating the certificate paths a config already holds is not a change, for
|
|
// the same reason restating any other value is not.
|
|
func TestWouldChangeIgnoresRestatedCertificatePaths(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "mtls.json")
|
|
_, err := UpdateOrCreateConfig(ConfigInput{
|
|
ConfigPath: path,
|
|
ManagementURL: "https://api.netbird.io:443",
|
|
ClientCertPath: "/etc/netbird/client.crt",
|
|
ClientCertKeyPath: "/etc/netbird/client.key",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
cfg, err := GetExistingConfig(path)
|
|
require.NoError(t, err)
|
|
|
|
changed, err := cfg.WouldChange(ConfigInput{
|
|
ClientCertPath: "/etc/netbird/client.crt",
|
|
ClientCertKeyPath: "/etc/netbird/client.key",
|
|
})
|
|
require.NoError(t, err)
|
|
require.False(t, changed, "the stored certificate paths were restated")
|
|
|
|
changed, err = cfg.WouldChange(ConfigInput{ClientCertPath: "/etc/netbird/other.crt"})
|
|
require.NoError(t, err)
|
|
require.True(t, changed, "a different certificate path is a change")
|
|
}
|
|
|
|
// A read that lands on a missing file must not hand back keys: nothing would
|
|
// write them down, so the caller would connect with an identity that changes on
|
|
// the next run and registers a second peer.
|
|
func TestReadOrGenerateConfigCarriesNoIdentity(t *testing.T) {
|
|
cfg, err := ReadOrGenerateConfig(filepath.Join(t.TempDir(), "absent.json"))
|
|
require.NoError(t, err)
|
|
|
|
require.Empty(t, cfg.PrivateKey, "a read minted a WireGuard key")
|
|
require.Empty(t, cfg.SSHKey, "a read minted an SSH key")
|
|
|
|
// So the caller's own EnsureIdentity is the one that reports the work, and
|
|
// therefore the one that triggers the write.
|
|
generated, err := cfg.EnsureIdentity()
|
|
require.NoError(t, err)
|
|
require.True(t, generated, "the provisioning caller could not tell it had to persist the identity")
|
|
}
|
|
|
|
// CreateInMemoryConfig is the opposite contract: its callers connect with what
|
|
// they get back, so it does carry an identity.
|
|
func TestCreateInMemoryConfigCarriesAnIdentity(t *testing.T) {
|
|
cfg, err := CreateInMemoryConfig(ConfigInput{ManagementURL: "https://api.netbird.io:443"})
|
|
require.NoError(t, err)
|
|
|
|
require.NotEmpty(t, cfg.PrivateKey)
|
|
require.NotEmpty(t, cfg.SSHKey)
|
|
}
|
|
|
|
// The admin panel is opened, not dialed, so its path identifies it. Comparing
|
|
// it as a bare endpoint left a custom panel URL unable to change.
|
|
func TestAdminURLPathIsPartOfTheIdentity(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "panel.json")
|
|
_, err := UpdateOrCreateConfig(ConfigInput{
|
|
ConfigPath: path,
|
|
AdminURL: "https://app.example.com/netbird",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
cfg, err := GetExistingConfig(path)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "https://app.example.com:443/netbird", cfg.AdminURL.String())
|
|
|
|
// Equivalent spellings of the same panel are still not a change.
|
|
for _, same := range []string{
|
|
"https://app.example.com/netbird",
|
|
"https://app.example.com:443/netbird",
|
|
"https://app.example.com/netbird/",
|
|
"https://APP.example.com/netbird",
|
|
} {
|
|
changed, err := cfg.WouldChange(ConfigInput{AdminURL: same})
|
|
require.NoError(t, err)
|
|
require.False(t, changed, "%q is the stored panel written differently", same)
|
|
}
|
|
|
|
// A different path is a different panel, and it must be persisted.
|
|
changed, err := cfg.WouldChange(ConfigInput{AdminURL: "https://app.example.com/other"})
|
|
require.NoError(t, err)
|
|
require.True(t, changed, "a different panel path is a change")
|
|
|
|
updated, err := UpdateConfig(ConfigInput{ConfigPath: path, AdminURL: "https://app.example.com/other"})
|
|
require.NoError(t, err)
|
|
require.Equal(t, "https://app.example.com:443/other", updated.AdminURL.String(), "the new panel path was not persisted")
|
|
}
|
|
|
|
// unsetOnDisk rewrites the stored config so the named fields carry a JSON null,
|
|
// which is how a profile written before apply() resolved them looks on disk.
|
|
// It synthesizes that state: no write produces it any more.
|
|
func unsetOnDisk(t *testing.T, path string, fields ...string) {
|
|
t.Helper()
|
|
|
|
raw, err := os.ReadFile(path)
|
|
require.NoError(t, err)
|
|
|
|
var stored map[string]json.RawMessage
|
|
require.NoError(t, json.Unmarshal(raw, &stored))
|
|
|
|
for _, field := range fields {
|
|
_, present := stored[field]
|
|
require.True(t, present, "%s is not a field of the stored config", field)
|
|
stored[field] = json.RawMessage("null")
|
|
}
|
|
|
|
rewritten, err := json.Marshal(stored)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(path, rewritten, 0600))
|
|
}
|
|
|
|
// Seven fields mean "the effective default" when they hold no value, and every
|
|
// profile written before apply() resolved them holds them as null. Restating
|
|
// that default is asking for no change — and the CLI restates it on every
|
|
// `netbird up`, because a flag set through an environment variable is a flag
|
|
// pflag reports as Changed. Judging those restatements as changes made the
|
|
// update-settings gate refuse `netbird up` outright for a client configured
|
|
// through the environment, which is the shape of a Kubernetes deployment.
|
|
//
|
|
// A login now writes those fields set, so the fixture puts the null state back
|
|
// on disk with unsetOnDisk instead of getting it from a login.
|
|
func TestWouldChangeIgnoresRestatedDefaultsOfUnsetFields(t *testing.T) {
|
|
networkMonitorDefault := runtime.GOOS == "windows" || runtime.GOOS == "darwin"
|
|
|
|
tests := []struct {
|
|
field string
|
|
theDefault ConfigInput
|
|
theOtherWay ConfigInput
|
|
}{
|
|
{"EnableSSHRoot",
|
|
ConfigInput{EnableSSHRoot: boolPtr(false)}, ConfigInput{EnableSSHRoot: boolPtr(true)}},
|
|
{"EnableSSHSFTP",
|
|
ConfigInput{EnableSSHSFTP: boolPtr(false)}, ConfigInput{EnableSSHSFTP: boolPtr(true)}},
|
|
{"EnableSSHLocalPortForwarding",
|
|
ConfigInput{EnableSSHLocalPortForwarding: boolPtr(false)}, ConfigInput{EnableSSHLocalPortForwarding: boolPtr(true)}},
|
|
{"EnableSSHRemotePortForwarding",
|
|
ConfigInput{EnableSSHRemotePortForwarding: boolPtr(false)}, ConfigInput{EnableSSHRemotePortForwarding: boolPtr(true)}},
|
|
{"DisableSSHAuth",
|
|
ConfigInput{DisableSSHAuth: boolPtr(false)}, ConfigInput{DisableSSHAuth: boolPtr(true)}},
|
|
{"SSHJWTCacheTTL",
|
|
ConfigInput{SSHJWTCacheTTL: intPtr(0)}, ConfigInput{SSHJWTCacheTTL: intPtr(300)}},
|
|
{"NetworkMonitor",
|
|
ConfigInput{NetworkMonitor: boolPtr(networkMonitorDefault)}, ConfigInput{NetworkMonitor: boolPtr(!networkMonitorDefault)}},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.field, func(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "unset.json")
|
|
_, err := UpdateOrCreateConfig(ConfigInput{
|
|
ConfigPath: path,
|
|
ManagementURL: "https://api.netbird.io:443",
|
|
})
|
|
require.NoError(t, err)
|
|
unsetOnDisk(t, path, tt.field)
|
|
|
|
cfg, err := GetExistingConfig(path)
|
|
require.NoError(t, err)
|
|
|
|
changed, err := cfg.WouldChange(tt.theDefault)
|
|
require.NoError(t, err)
|
|
require.False(t, changed, "restating the default of an unset %s was judged a change", tt.field)
|
|
|
|
// The gate still has to refuse a request that does ask for something.
|
|
changed, err = cfg.WouldChange(tt.theOtherWay)
|
|
require.NoError(t, err)
|
|
require.True(t, changed, "asking for a non-default %s is a change", tt.field)
|
|
})
|
|
}
|
|
}
|
|
|
|
// The verdict must not depend on where the caller got the config from. Readers
|
|
// normalize what they hand out, but apply() signals "I filled in a default"
|
|
// through the same bool as "the input changed something", so a config that
|
|
// never passed through a read would otherwise report a change for an input
|
|
// that asks for nothing.
|
|
func TestWouldChangeNormalizesBeforeMeasuring(t *testing.T) {
|
|
rawConfig := func(t *testing.T) *Config {
|
|
t.Helper()
|
|
|
|
cfg := &Config{WgIface: iface.WgInterfaceDefault}
|
|
require.Nil(t, cfg.ServerSSHAllowed, "the fixture is only useful while the config is not normalized")
|
|
require.Nil(t, cfg.EnableSSHRoot)
|
|
require.Empty(t, cfg.IFaceBlackList)
|
|
return cfg
|
|
}
|
|
|
|
changed, err := rawConfig(t).WouldChange(ConfigInput{})
|
|
require.NoError(t, err)
|
|
require.False(t, changed, "an input carrying nothing cannot change anything")
|
|
|
|
changed, err = rawConfig(t).WouldChange(ConfigInput{EnableSSHRoot: boolPtr(false)})
|
|
require.NoError(t, err)
|
|
require.False(t, changed, "the default of a field the config never held is not a change")
|
|
|
|
changed, err = rawConfig(t).WouldChange(ConfigInput{EnableSSHRoot: boolPtr(true)})
|
|
require.NoError(t, err)
|
|
require.True(t, changed, "a non-default value is still a change")
|
|
}
|
|
|
|
// A zero-padded port addresses the same port. The normalization itself belongs
|
|
// to util.ServiceURLPort and is tested there; this asserts that the comparison
|
|
// this package hands its callers inherits it.
|
|
func TestServiceURLPortIsNormalizedNumerically(t *testing.T) {
|
|
padded, err := ParseServiceURL("padded", "https://mgmt.example.com:0443")
|
|
require.NoError(t, err)
|
|
plain, err := ParseServiceURL("plain", "https://mgmt.example.com:443")
|
|
require.NoError(t, err)
|
|
|
|
require.True(t, SameServiceURL(padded, plain))
|
|
}
|