kubo/config/types_test.go
Marcin Rataj 93f8897d7c
Some checks are pending
CodeQL / codeql (push) Waiting to run
Docker Check / lint (push) Waiting to run
Docker Check / build (push) Waiting to run
Gateway Conformance / gateway-conformance (push) Waiting to run
Gateway Conformance / gateway-conformance-libp2p-experiment (push) Waiting to run
Go Build / go-build (push) Waiting to run
Go Check / go-check (push) Waiting to run
Go Lint / go-lint (push) Waiting to run
Go Test / go-test (push) Waiting to run
Interop / interop-prep (push) Waiting to run
Interop / helia-interop (push) Blocked by required conditions
Interop / ipfs-webui (push) Blocked by required conditions
Sharness / sharness-test (push) Waiting to run
Spell Check / spellcheck (push) Waiting to run
feat(config): optional Gateway.MaxRangeRequestFileSize (#10997)
adds Gateway.MaxRangeRequestFileSize configuration to protect against CDN bugs
where range requests over certain sizes return entire files instead of requested
byte ranges, causing unexpected bandwidth costs.

- default: 0 (no limit)
- returns 501 Not Implemented for oversized range requests
- protects against CDNs like Cloudflare that ignore range requests over 5GiB

also introduces OptionalBytes type to reduce code duplication when handling
byte-size configuration values, replacing manual string parsing with humanize.ParseBytes.
migrates existing byte-size configs to use this new type.

Fixes: https://github.com/ipfs/boxo/issues/856
2025-11-11 18:54:43 -08:00

637 lines
16 KiB
Go

package config
import (
"bytes"
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestOptionalDuration(t *testing.T) {
makeDurationPointer := func(d time.Duration) *time.Duration { return &d }
t.Run("marshalling and unmarshalling", func(t *testing.T) {
out, err := json.Marshal(OptionalDuration{value: makeDurationPointer(time.Second)})
if err != nil {
t.Fatal(err)
}
expected := "\"1s\""
if string(out) != expected {
t.Fatalf("expected %s, got %s", expected, string(out))
}
var d OptionalDuration
if err := json.Unmarshal(out, &d); err != nil {
t.Fatal(err)
}
if *d.value != time.Second {
t.Fatal("expected a second")
}
})
t.Run("default value", func(t *testing.T) {
for _, jsonStr := range []string{"null", "\"null\"", "\"\"", "\"default\""} {
var d OptionalDuration
if !d.IsDefault() {
t.Fatal("expected value to be the default initially")
}
if err := json.Unmarshal([]byte(jsonStr), &d); err != nil {
t.Fatalf("%s failed to unmarshall with %s", jsonStr, err)
}
if dur := d.WithDefault(time.Hour); dur != time.Hour {
t.Fatalf("expected default value to be used, got %s", dur)
}
if !d.IsDefault() {
t.Fatal("expected value to be the default")
}
}
})
t.Run("omitempty with default value", func(t *testing.T) {
type Foo struct {
D *OptionalDuration `json:",omitempty"`
}
// marshall to JSON without empty field
out, err := json.Marshal(new(Foo))
if err != nil {
t.Fatal(err)
}
if string(out) != "{}" {
t.Fatalf("expected omitempty to omit the duration, got %s", out)
}
// unmarshall missing value and get the default
var foo2 Foo
if err := json.Unmarshal(out, &foo2); err != nil {
t.Fatalf("%s failed to unmarshall with %s", string(out), err)
}
if dur := foo2.D.WithDefault(time.Hour); dur != time.Hour {
t.Fatalf("expected default value to be used, got %s", dur)
}
if !foo2.D.IsDefault() {
t.Fatal("expected value to be the default")
}
})
t.Run("roundtrip including the default values", func(t *testing.T) {
for jsonStr, goValue := range map[string]OptionalDuration{
// there are various footguns user can hit, normalize them to the canonical default
"null": {}, // JSON null → default value
"\"null\"": {}, // JSON string "null" sent/set by "ipfs config" cli → default value
"\"default\"": {}, // explicit "default" as string
"\"\"": {}, // user removed custom value, empty string should also parse as default
"\"1s\"": {value: makeDurationPointer(time.Second)},
"\"42h1m3s\"": {value: makeDurationPointer(42*time.Hour + 1*time.Minute + 3*time.Second)},
} {
var d OptionalDuration
err := json.Unmarshal([]byte(jsonStr), &d)
if err != nil {
t.Fatal(err)
}
if goValue.value == nil && d.value == nil {
} else if goValue.value == nil && d.value != nil {
t.Errorf("expected nil for %s, got %s", jsonStr, d)
} else if *d.value != *goValue.value {
t.Fatalf("expected %s for %s, got %s", goValue, jsonStr, d)
}
// Test Reverse
out, err := json.Marshal(goValue)
if err != nil {
t.Fatal(err)
}
if goValue.value == nil {
if !bytes.Equal(out, []byte("null")) {
t.Fatalf("expected JSON null for %s, got %s", jsonStr, string(out))
}
continue
}
if string(out) != jsonStr {
t.Fatalf("expected %s, got %s", jsonStr, string(out))
}
}
})
t.Run("invalid duration values", func(t *testing.T) {
for _, invalid := range []string{
"\"s\"", "\"1ę\"", "\"-1\"", "\"1H\"", "\"day\"",
} {
var d OptionalDuration
err := json.Unmarshal([]byte(invalid), &d)
if err == nil {
t.Errorf("expected to fail to decode %s as an OptionalDuration, got %s instead", invalid, d)
}
}
})
}
func TestOneStrings(t *testing.T) {
out, err := json.Marshal(Strings{"one"})
if err != nil {
t.Fatal(err)
}
expected := "\"one\""
if string(out) != expected {
t.Fatalf("expected %s, got %s", expected, string(out))
}
}
func TestNoStrings(t *testing.T) {
out, err := json.Marshal(Strings{})
if err != nil {
t.Fatal(err)
}
expected := "null"
if string(out) != expected {
t.Fatalf("expected %s, got %s", expected, string(out))
}
}
func TestManyStrings(t *testing.T) {
out, err := json.Marshal(Strings{"one", "two"})
if err != nil {
t.Fatal(err)
}
expected := "[\"one\",\"two\"]"
if string(out) != expected {
t.Fatalf("expected %s, got %s", expected, string(out))
}
}
func TestFunkyStrings(t *testing.T) {
toParse := " [ \"one\", \"two\" ] "
var s Strings
if err := json.Unmarshal([]byte(toParse), &s); err != nil {
t.Fatal(err)
}
if len(s) != 2 || s[0] != "one" && s[1] != "two" {
t.Fatalf("unexpected result: %v", s)
}
}
func TestFlag(t *testing.T) {
// make sure we have the right zero value.
var defaultFlag Flag
if defaultFlag != Default {
t.Errorf("expected default flag to be %q, got %q", Default, defaultFlag)
}
if defaultFlag.WithDefault(true) != true {
t.Error("expected default & true to be true")
}
if defaultFlag.WithDefault(false) != false {
t.Error("expected default & false to be false")
}
if True.WithDefault(false) != true {
t.Error("default should only apply to default")
}
if False.WithDefault(true) != false {
t.Error("default should only apply to default")
}
if True.WithDefault(true) != true {
t.Error("true & true is true")
}
if False.WithDefault(true) != false {
t.Error("false & false is false")
}
for jsonStr, goValue := range map[string]Flag{
"null": Default,
"true": True,
"false": False,
} {
var d Flag
err := json.Unmarshal([]byte(jsonStr), &d)
if err != nil {
t.Fatal(err)
}
if d != goValue {
t.Fatalf("expected %s, got %s", goValue, d)
}
// Reverse
out, err := json.Marshal(goValue)
if err != nil {
t.Fatal(err)
}
if string(out) != jsonStr {
t.Fatalf("expected %s, got %s", jsonStr, string(out))
}
}
type Foo struct {
F Flag `json:",omitempty"`
}
out, err := json.Marshal(new(Foo))
if err != nil {
t.Fatal(err)
}
expected := "{}"
if string(out) != expected {
t.Fatal("expected omitempty to omit the flag")
}
}
func TestPriority(t *testing.T) {
// make sure we have the right zero value.
var defaultPriority Priority
if defaultPriority != DefaultPriority {
t.Errorf("expected default priority to be %q, got %q", DefaultPriority, defaultPriority)
}
if _, ok := defaultPriority.WithDefault(Disabled); ok {
t.Error("should have been disabled")
}
if p, ok := defaultPriority.WithDefault(1); !ok || p != 1 {
t.Errorf("priority should have been 1, got %d", p)
}
if p, ok := defaultPriority.WithDefault(DefaultPriority); !ok || p != 0 {
t.Errorf("priority should have been 0, got %d", p)
}
for jsonStr, goValue := range map[string]Priority{
"null": DefaultPriority,
"false": Disabled,
"1": 1,
"2": 2,
"100": 100,
} {
var d Priority
err := json.Unmarshal([]byte(jsonStr), &d)
if err != nil {
t.Fatal(err)
}
if d != goValue {
t.Fatalf("expected %s, got %s", goValue, d)
}
// Reverse
out, err := json.Marshal(goValue)
if err != nil {
t.Fatal(err)
}
if string(out) != jsonStr {
t.Fatalf("expected %s, got %s", jsonStr, string(out))
}
}
type Foo struct {
P Priority `json:",omitempty"`
}
out, err := json.Marshal(new(Foo))
if err != nil {
t.Fatal(err)
}
expected := "{}"
if string(out) != expected {
t.Fatal("expected omitempty to omit the flag")
}
for _, invalid := range []string{
"0", "-1", "-2", "1.1", "0.0",
} {
var p Priority
err := json.Unmarshal([]byte(invalid), &p)
if err == nil {
t.Errorf("expected to fail to decode %s as a priority", invalid)
}
}
}
func TestOptionalInteger(t *testing.T) {
makeInt64Pointer := func(v int64) *int64 {
return &v
}
var defaultOptionalInt OptionalInteger
if !defaultOptionalInt.IsDefault() {
t.Fatal("should be the default")
}
if val := defaultOptionalInt.WithDefault(0); val != 0 {
t.Errorf("optional integer should have been 0, got %d", val)
}
if val := defaultOptionalInt.WithDefault(1); val != 1 {
t.Errorf("optional integer should have been 1, got %d", val)
}
if val := defaultOptionalInt.WithDefault(-1); val != -1 {
t.Errorf("optional integer should have been -1, got %d", val)
}
var filledInt OptionalInteger
filledInt = OptionalInteger{value: makeInt64Pointer(1)}
if filledInt.IsDefault() {
t.Fatal("should not be the default")
}
if val := filledInt.WithDefault(0); val != 1 {
t.Errorf("optional integer should have been 1, got %d", val)
}
if val := filledInt.WithDefault(-1); val != 1 {
t.Errorf("optional integer should have been 1, got %d", val)
}
filledInt = OptionalInteger{value: makeInt64Pointer(0)}
if val := filledInt.WithDefault(1); val != 0 {
t.Errorf("optional integer should have been 0, got %d", val)
}
for jsonStr, goValue := range map[string]OptionalInteger{
"null": {},
"0": {value: makeInt64Pointer(0)},
"1": {value: makeInt64Pointer(1)},
"-1": {value: makeInt64Pointer(-1)},
} {
var d OptionalInteger
err := json.Unmarshal([]byte(jsonStr), &d)
if err != nil {
t.Fatal(err)
}
if goValue.value == nil && d.value == nil {
} else if goValue.value == nil && d.value != nil {
t.Errorf("expected default, got %s", d)
} else if *d.value != *goValue.value {
t.Fatalf("expected %s, got %s", goValue, d)
}
// Reverse
out, err := json.Marshal(goValue)
if err != nil {
t.Fatal(err)
}
if string(out) != jsonStr {
t.Fatalf("expected %s, got %s", jsonStr, string(out))
}
}
// marshal with omitempty
type Foo struct {
I *OptionalInteger `json:",omitempty"`
}
out, err := json.Marshal(new(Foo))
if err != nil {
t.Fatal(err)
}
expected := "{}"
if string(out) != expected {
t.Fatal("expected omitempty to omit the optional integer")
}
// unmarshal from omitempty output and get default value
var foo2 Foo
if err := json.Unmarshal(out, &foo2); err != nil {
t.Fatalf("%s failed to unmarshall with %s", string(out), err)
}
if i := foo2.I.WithDefault(42); i != 42 {
t.Fatalf("expected default value to be used, got %d", i)
}
if !foo2.I.IsDefault() {
t.Fatal("expected value to be the default")
}
// test invalid values
for _, invalid := range []string{
"foo", "-1.1", "1.1", "0.0", "[]",
} {
var p OptionalInteger
err := json.Unmarshal([]byte(invalid), &p)
if err == nil {
t.Errorf("expected to fail to decode %s as a priority", invalid)
}
}
}
func TestOptionalString(t *testing.T) {
makeStringPointer := func(v string) *string {
return &v
}
var defaultOptionalString OptionalString
if !defaultOptionalString.IsDefault() {
t.Fatal("should be the default")
}
if val := defaultOptionalString.WithDefault(""); val != "" {
t.Errorf("optional string should have been empty, got %s", val)
}
if val := defaultOptionalString.String(); val != "default" {
t.Fatalf("default optional string should be the 'default' string, got %s", val)
}
if val := defaultOptionalString.WithDefault("foo"); val != "foo" {
t.Errorf("optional string should have been foo, got %s", val)
}
var filledStr OptionalString
filledStr = OptionalString{value: makeStringPointer("foo")}
if filledStr.IsDefault() {
t.Fatal("should not be the default")
}
if val := filledStr.WithDefault("bar"); val != "foo" {
t.Errorf("optional string should have been foo, got %s", val)
}
if val := filledStr.String(); val != "foo" {
t.Fatalf("optional string should have been foo, got %s", val)
}
filledStr = OptionalString{value: makeStringPointer("")}
if val := filledStr.WithDefault("foo"); val != "" {
t.Errorf("optional string should have been 0, got %s", val)
}
for jsonStr, goValue := range map[string]OptionalString{
"null": {},
"\"0\"": {value: makeStringPointer("0")},
"\"\"": {value: makeStringPointer("")},
`"1"`: {value: makeStringPointer("1")},
`"-1"`: {value: makeStringPointer("-1")},
`"qwerty"`: {value: makeStringPointer("qwerty")},
} {
var d OptionalString
err := json.Unmarshal([]byte(jsonStr), &d)
if err != nil {
t.Fatal(err)
}
if goValue.value == nil && d.value == nil {
} else if goValue.value == nil && d.value != nil {
t.Errorf("expected default, got %s", d)
} else if *d.value != *goValue.value {
t.Fatalf("expected %s, got %s", goValue, d)
}
// Reverse
out, err := json.Marshal(goValue)
if err != nil {
t.Fatal(err)
}
if string(out) != jsonStr {
t.Fatalf("expected %s, got %s", jsonStr, string(out))
}
}
// marshal with omitempty
type Foo struct {
S *OptionalString `json:",omitempty"`
}
out, err := json.Marshal(new(Foo))
if err != nil {
t.Fatal(err)
}
expected := "{}"
if string(out) != expected {
t.Fatal("expected omitempty to omit the optional integer")
}
// unmarshal from omitempty output and get default value
var foo2 Foo
if err := json.Unmarshal(out, &foo2); err != nil {
t.Fatalf("%s failed to unmarshall with %s", string(out), err)
}
if s := foo2.S.WithDefault("foo"); s != "foo" {
t.Fatalf("expected default value to be used, got %s", s)
}
if !foo2.S.IsDefault() {
t.Fatal("expected value to be the default")
}
for _, invalid := range []string{
"[]", "{}", "0", "a", "'b'",
} {
var p OptionalString
err := json.Unmarshal([]byte(invalid), &p)
if err == nil {
t.Errorf("expected to fail to decode %s as an optional string", invalid)
}
}
}
func TestOptionalBytes(t *testing.T) {
makeStringPointer := func(v string) *string { return &v }
t.Run("default value", func(t *testing.T) {
var b OptionalBytes
assert.True(t, b.IsDefault())
assert.Equal(t, uint64(0), b.WithDefault(0))
assert.Equal(t, uint64(1024), b.WithDefault(1024))
assert.Equal(t, "default", b.String())
})
t.Run("non-default value", func(t *testing.T) {
b := OptionalBytes{OptionalString{value: makeStringPointer("1MiB")}}
assert.False(t, b.IsDefault())
assert.Equal(t, uint64(1048576), b.WithDefault(512))
assert.Equal(t, "1MiB", b.String())
})
t.Run("JSON roundtrip", func(t *testing.T) {
testCases := []struct {
jsonInput string
jsonOutput string
expectedValue string
}{
{"null", "null", ""},
{"\"256KiB\"", "\"256KiB\"", "256KiB"},
{"\"1MiB\"", "\"1MiB\"", "1MiB"},
{"\"5GiB\"", "\"5GiB\"", "5GiB"},
{"\"256KB\"", "\"256KB\"", "256KB"},
{"1048576", "\"1048576\"", "1048576"},
}
for _, tc := range testCases {
t.Run(tc.jsonInput, func(t *testing.T) {
var b OptionalBytes
err := json.Unmarshal([]byte(tc.jsonInput), &b)
require.NoError(t, err)
if tc.expectedValue == "" {
assert.Nil(t, b.value)
} else {
require.NotNil(t, b.value)
assert.Equal(t, tc.expectedValue, *b.value)
}
out, err := json.Marshal(b)
require.NoError(t, err)
assert.Equal(t, tc.jsonOutput, string(out))
})
}
})
t.Run("parsing byte sizes", func(t *testing.T) {
testCases := []struct {
input string
expected uint64
}{
{"256KiB", 262144},
{"1MiB", 1048576},
{"5GiB", 5368709120},
{"256KB", 256000},
{"1048576", 1048576},
}
for _, tc := range testCases {
t.Run(tc.input, func(t *testing.T) {
var b OptionalBytes
err := json.Unmarshal([]byte("\""+tc.input+"\""), &b)
require.NoError(t, err)
assert.Equal(t, tc.expected, b.WithDefault(0))
})
}
})
t.Run("omitempty", func(t *testing.T) {
type Foo struct {
B *OptionalBytes `json:",omitempty"`
}
out, err := json.Marshal(new(Foo))
require.NoError(t, err)
assert.Equal(t, "{}", string(out))
var foo2 Foo
err = json.Unmarshal(out, &foo2)
require.NoError(t, err)
if foo2.B != nil {
assert.Equal(t, uint64(1024), foo2.B.WithDefault(1024))
assert.True(t, foo2.B.IsDefault())
} else {
// When field is omitted, pointer is nil which is also considered default
t.Log("B is nil, which is acceptable for omitempty")
}
})
t.Run("invalid values", func(t *testing.T) {
invalidInputs := []string{
"\"5XiB\"", "\"invalid\"", "\"\"", "[]", "{}",
}
for _, invalid := range invalidInputs {
t.Run(invalid, func(t *testing.T) {
var b OptionalBytes
err := json.Unmarshal([]byte(invalid), &b)
assert.Error(t, err)
})
}
})
t.Run("panic on invalid stored value", func(t *testing.T) {
// This tests that if somehow an invalid value gets stored
// (bypassing UnmarshalJSON validation), WithDefault will panic
invalidValue := "invalid-size"
b := OptionalBytes{OptionalString{value: &invalidValue}}
assert.Panics(t, func() {
b.WithDefault(1024)
}, "should panic on invalid stored value")
})
}