234 lines
8.2 KiB
Go
234 lines
8.2 KiB
Go
package config
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"math"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestLoadConfigOptionalMissingFallbackAppliesCredentialInFlightDefaults(t *testing.T) {
|
|
cfg, errLoad := LoadConfigOptional(filepath.Join(t.TempDir(), "missing.yaml"), true)
|
|
if errLoad != nil {
|
|
t.Fatalf("LoadConfigOptional() error = %v", errLoad)
|
|
}
|
|
assertOptionalConfigFallback(t, cfg)
|
|
}
|
|
|
|
func TestLoadConfigOptionalEmptyFallbackAppliesCredentialInFlightDefaults(t *testing.T) {
|
|
configPath := filepath.Join(t.TempDir(), "config.yaml")
|
|
if errWrite := os.WriteFile(configPath, nil, 0o600); errWrite != nil {
|
|
t.Fatal(errWrite)
|
|
}
|
|
cfg, errLoad := LoadConfigOptional(configPath, true)
|
|
if errLoad != nil {
|
|
t.Fatalf("LoadConfigOptional() error = %v", errLoad)
|
|
}
|
|
assertOptionalConfigFallback(t, cfg)
|
|
}
|
|
|
|
func TestLoadConfigOptionalWhitespaceFallbackAppliesCredentialInFlightDefaults(t *testing.T) {
|
|
configPath := filepath.Join(t.TempDir(), "config.yaml")
|
|
if errWrite := os.WriteFile(configPath, []byte(" \t\n\r "), 0o600); errWrite != nil {
|
|
t.Fatal(errWrite)
|
|
}
|
|
cfg, errLoad := LoadConfigOptional(configPath, true)
|
|
if errLoad != nil {
|
|
t.Fatalf("LoadConfigOptional() error = %v", errLoad)
|
|
}
|
|
assertOptionalConfigFallback(t, cfg)
|
|
}
|
|
|
|
func TestLoadConfigOptionalInvalidFallbackAppliesCredentialInFlightDefaults(t *testing.T) {
|
|
configPath := filepath.Join(t.TempDir(), "config.yaml")
|
|
if errWrite := os.WriteFile(configPath, []byte(":"), 0o600); errWrite != nil {
|
|
t.Fatal(errWrite)
|
|
}
|
|
cfg, errLoad := LoadConfigOptional(configPath, true)
|
|
if errLoad != nil {
|
|
t.Fatalf("LoadConfigOptional() error = %v", errLoad)
|
|
}
|
|
assertOptionalConfigFallback(t, cfg)
|
|
}
|
|
|
|
func assertOptionalConfigFallback(t *testing.T, cfg *Config) {
|
|
t.Helper()
|
|
if cfg.CredentialInFlight != DefaultCredentialInFlightConfig() {
|
|
t.Fatalf("CredentialInFlight = %#v, want %#v", cfg.CredentialInFlight, DefaultCredentialInFlightConfig())
|
|
}
|
|
if errValidate := cfg.CredentialInFlight.Validate(); errValidate != nil {
|
|
t.Fatalf("CredentialInFlight.Validate() error = %v", errValidate)
|
|
}
|
|
if cfg.ErrorLogsMaxFiles != 0 || cfg.WebsocketAuth || cfg.CredentialConcurrency != (CredentialConcurrencyConfig{}) {
|
|
t.Fatalf("fallback config changed existing empty-config defaults: %#v", cfg)
|
|
}
|
|
}
|
|
|
|
func TestCredentialInFlightConfigContractFixture(t *testing.T) {
|
|
raw, errRead := os.ReadFile(filepath.Join("..", "home", "testdata", "credential_in_flight_contract.json"))
|
|
if errRead != nil {
|
|
t.Fatal(errRead)
|
|
}
|
|
fixture, errDecode := decodeCredentialInFlightConfigFixture(raw)
|
|
if errDecode != nil {
|
|
t.Fatal(errDecode)
|
|
}
|
|
if fixture.Config != DefaultCredentialInFlightConfig() {
|
|
t.Fatalf("default config = %#v, want %#v", DefaultCredentialInFlightConfig(), fixture.Config)
|
|
}
|
|
if errValidate := fixture.Config.Validate(); errValidate != nil {
|
|
t.Fatalf("Validate() error = %v", errValidate)
|
|
}
|
|
assertCredentialInFlightConfigFields(t)
|
|
assertRequiredJSONKeys(t, raw, []string{"config", "part", "overflow"})
|
|
assertRequiredJSONKeys(t, fixture.ConfigJSON, []string{"snapshot-interval", "stale-after", "max-part-bytes", "max-part-count", "max-revision-bytes", "max-aggregate-groups", "max-details", "max-string-bytes", "staging-retention"})
|
|
}
|
|
|
|
func TestCredentialInFlightConfigFixtureRejectsInvalidJSON(t *testing.T) {
|
|
raw, errRead := os.ReadFile(filepath.Join("..", "home", "testdata", "credential_in_flight_contract.json"))
|
|
if errRead != nil {
|
|
t.Fatal(errRead)
|
|
}
|
|
for _, test := range []struct {
|
|
name string
|
|
raw []byte
|
|
}{
|
|
{name: "unknown config field", raw: bytes.Replace(raw, []byte(`"snapshot-interval": "2s"`), []byte(`"snapshot-interval": "2s", "secret": "secret"`), 1)},
|
|
{name: "trailing JSON", raw: append(append([]byte{}, raw...), []byte(` {"config": {}}`)...)},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
if _, errDecode := decodeCredentialInFlightConfigFixture(test.raw); errDecode == nil {
|
|
t.Fatal("decodeCredentialInFlightConfigFixture() error = nil")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCredentialInFlightConfigDurationBounds(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
stale string
|
|
every string
|
|
valid bool
|
|
}{
|
|
{name: "exact three intervals", every: "1s", stale: "3s", valid: true},
|
|
{name: "below three intervals", every: "1s", stale: "2999999999ns", valid: false},
|
|
{name: "near duration maximum", every: time.Duration(math.MaxInt64 / 2).String(), stale: time.Duration(math.MaxInt64).String(), valid: false},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
cfg := DefaultCredentialInFlightConfig()
|
|
cfg.SnapshotInterval = test.every
|
|
cfg.StaleAfter = test.stale
|
|
errValidate := cfg.Validate()
|
|
if (errValidate == nil) != test.valid {
|
|
t.Fatalf("Validate() error = %v, want valid = %t", errValidate, test.valid)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCredentialInFlightConfigRejectsUnsafeBounds(t *testing.T) {
|
|
cfg := DefaultCredentialInFlightConfig()
|
|
cfg.StaleAfter = "5s"
|
|
if errValidate := cfg.Validate(); errValidate == nil {
|
|
t.Fatal("Validate() error = nil, want stale-after error")
|
|
}
|
|
cfg = DefaultCredentialInFlightConfig()
|
|
cfg.MaxRevisionBytes = 16*1024*1024 + 1
|
|
if errValidate := cfg.Validate(); errValidate == nil {
|
|
t.Fatal("Validate() error = nil, want hard revision bound error")
|
|
}
|
|
cfg = DefaultCredentialInFlightConfig()
|
|
cfg.MaxPartBytes = math.MaxInt
|
|
if errValidate := cfg.Validate(); errValidate == nil {
|
|
t.Fatal("Validate() error = nil, want overflow-safe part bound error")
|
|
}
|
|
}
|
|
|
|
type credentialInFlightConfigFixture struct {
|
|
Config CredentialInFlightConfig `json:"config"`
|
|
ConfigJSON json.RawMessage `json:"-"`
|
|
}
|
|
|
|
func decodeCredentialInFlightConfigFixture(raw []byte) (credentialInFlightConfigFixture, error) {
|
|
var fixture credentialInFlightConfigFixture
|
|
var document struct {
|
|
Config json.RawMessage `json:"config"`
|
|
Part json.RawMessage `json:"part"`
|
|
Overflow json.RawMessage `json:"overflow"`
|
|
}
|
|
decoder := json.NewDecoder(bytes.NewReader(raw))
|
|
decoder.DisallowUnknownFields()
|
|
if errDecode := decoder.Decode(&document); errDecode != nil {
|
|
return fixture, errDecode
|
|
}
|
|
if errDecode := decoder.Decode(&struct{}{}); errDecode == nil {
|
|
return fixture, errors.New("unexpected trailing JSON")
|
|
} else if errDecode != io.EOF {
|
|
return fixture, errDecode
|
|
}
|
|
decoder = json.NewDecoder(bytes.NewReader(document.Config))
|
|
decoder.DisallowUnknownFields()
|
|
if errDecode := decoder.Decode(&fixture.Config); errDecode != nil {
|
|
return fixture, errDecode
|
|
}
|
|
if errDecode := decoder.Decode(&struct{}{}); errDecode == nil {
|
|
return fixture, errors.New("unexpected trailing config JSON")
|
|
} else if errDecode != io.EOF {
|
|
return fixture, errDecode
|
|
}
|
|
fixture.ConfigJSON = document.Config
|
|
return fixture, nil
|
|
}
|
|
|
|
func assertCredentialInFlightConfigFields(t *testing.T) {
|
|
t.Helper()
|
|
assertOrderedJSONFields(t, reflect.TypeOf(CredentialInFlightConfig{}), []jsonField{
|
|
{name: "SnapshotInterval", tag: "snapshot-interval"},
|
|
{name: "StaleAfter", tag: "stale-after"},
|
|
{name: "MaxPartBytes", tag: "max-part-bytes"},
|
|
{name: "MaxPartCount", tag: "max-part-count"},
|
|
{name: "MaxRevisionBytes", tag: "max-revision-bytes"},
|
|
{name: "MaxAggregateGroups", tag: "max-aggregate-groups"},
|
|
{name: "MaxDetails", tag: "max-details"},
|
|
{name: "MaxStringBytes", tag: "max-string-bytes"},
|
|
{name: "StagingRetention", tag: "staging-retention"},
|
|
})
|
|
}
|
|
|
|
type jsonField struct {
|
|
name string
|
|
tag string
|
|
}
|
|
|
|
func assertOrderedJSONFields(t *testing.T, structType reflect.Type, want []jsonField) {
|
|
t.Helper()
|
|
if structType.NumField() != len(want) {
|
|
t.Fatalf("%s field count = %d, want %d", structType.Name(), structType.NumField(), len(want))
|
|
}
|
|
for index, expected := range want {
|
|
field := structType.Field(index)
|
|
if field.Name != expected.name || field.Tag.Get("json") != expected.tag {
|
|
t.Fatalf("%s field %d = (%q, %q), want (%q, %q)", structType.Name(), index, field.Name, field.Tag.Get("json"), expected.name, expected.tag)
|
|
}
|
|
}
|
|
}
|
|
|
|
func assertRequiredJSONKeys(t *testing.T, raw json.RawMessage, required []string) {
|
|
t.Helper()
|
|
var fields map[string]json.RawMessage
|
|
if errDecode := json.Unmarshal(raw, &fields); errDecode != nil {
|
|
t.Fatalf("json.Unmarshal() error = %v", errDecode)
|
|
}
|
|
for _, key := range required {
|
|
if _, ok := fields[key]; !ok {
|
|
t.Fatalf("required JSON key %q is missing", key)
|
|
}
|
|
}
|
|
}
|