Files
akvorado/cmd/config_test.go

621 lines
14 KiB
Go

// SPDX-FileCopyrightText: 2022 Free Mobile
// SPDX-License-Identifier: AGPL-3.0-only
package cmd_test
import (
"bytes"
"fmt"
"log"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/gin-gonic/gin"
"akvorado/common/helpers/yaml"
"akvorado/cmd"
"akvorado/common/helpers"
)
type dummyConfiguration struct {
Module1 dummyModule1Configuration
Module2 dummyModule2Configuration
}
type dummyModule1Configuration struct {
Listen string `validate:"listen"`
Topic string `validate:"gte=3"`
Workers int `validate:"gte=1"`
}
type dummyModule2Configuration struct {
Details dummyModule2DetailsConfiguration
Elements []dummyModule2ElementsConfiguration
MoreDetails `mapstructure:",squash" yaml:",inline"`
}
type MoreDetails struct {
Stuff string
}
type dummyModule2ElementsConfiguration struct {
Name string
Gauge int
}
type dummyModule2DetailsConfiguration struct {
Workers int
IntervalValue time.Duration
}
func (c *dummyConfiguration) Reset() {
*c = dummyConfiguration{
Module1: dummyModule1Configuration{
Listen: "127.0.0.1:8080",
Topic: "nothingness",
Workers: 100,
},
Module2: dummyModule2Configuration{
MoreDetails: MoreDetails{
Stuff: "hello",
},
Details: dummyModule2DetailsConfiguration{
Workers: 1,
IntervalValue: time.Minute,
},
Elements: []dummyModule2ElementsConfiguration{
{
Name: "el1",
Gauge: 10,
}, {
Name: "el2",
Gauge: 11,
},
},
},
}
}
func TestValidation(t *testing.T) {
config := `---
module1:
topic: fl
workers: -5
`
configFile := filepath.Join(t.TempDir(), "config.yaml")
os.WriteFile(configFile, []byte(config), 0o644)
c := cmd.ConfigRelatedOptions{
Path: configFile,
}
parsed := dummyConfiguration{}
out := bytes.NewBuffer([]byte{})
if _, err := c.Parse(out, "dummy", &parsed); err == nil {
t.Fatal("Parse() didn't error")
} else if diff := helpers.Diff(err.Error(), `invalid configuration:
Key: 'dummyConfiguration.Module1.Topic' Error:Field validation for 'Topic' failed on the 'gte' tag
Key: 'dummyConfiguration.Module1.Workers' Error:Field validation for 'Workers' failed on the 'gte' tag`); diff != "" {
t.Fatalf("Parse() (-got, +want):\n%s", diff)
}
}
func TestDump(t *testing.T) {
// Configuration file
config := `---
module1:
topic: flows
module2:
details:
workers: 5
interval-value: 20m
stuff: bye
elements:
- name: first
gauge: 67
- name: second
`
configFile := filepath.Join(t.TempDir(), "config.yaml")
os.WriteFile(configFile, []byte(config), 0o644)
c := cmd.ConfigRelatedOptions{
Path: configFile,
Dump: true,
}
parsed := dummyConfiguration{}
out := bytes.NewBuffer([]byte{})
if _, err := c.Parse(out, "dummy", &parsed); err != nil {
t.Fatalf("Parse() error:\n%+v", err)
}
// Expected configuration
expected := dummyConfiguration{
Module1: dummyModule1Configuration{
Listen: "127.0.0.1:8080",
Topic: "flows",
Workers: 100,
},
Module2: dummyModule2Configuration{
MoreDetails: MoreDetails{
Stuff: "bye",
},
Details: dummyModule2DetailsConfiguration{
Workers: 5,
IntervalValue: 20 * time.Minute,
},
Elements: []dummyModule2ElementsConfiguration{
{"first", 67},
{"second", 0},
},
},
}
if diff := helpers.Diff(parsed, expected); diff != "" {
t.Errorf("Parse() (-got, +want):\n%s", diff)
}
var gotRaw map[string]gin.H
if err := yaml.Unmarshal(out.Bytes(), &gotRaw); err != nil {
t.Fatalf("Unmarshal() error:\n%+v", err)
}
expectedRaw := map[string]gin.H{
"module1": {
"listen": "127.0.0.1:8080",
"topic": "flows",
"workers": 100,
},
"module2": {
"stuff": "bye",
"details": gin.H{
"workers": 5,
"intervalvalue": "20m0s",
},
"elements": []any{
gin.H{
"name": "first",
"gauge": 67,
},
gin.H{
"name": "second",
"gauge": 0,
},
},
},
}
if diff := helpers.Diff(gotRaw, expectedRaw); diff != "" {
t.Errorf("Parse() (-got, +want):\n%s", diff)
}
}
func TestEnvOverride(t *testing.T) {
// Configuration file
config := `---
module1:
topic: flows
module2:
details:
workers: 5
interval-value: 20m
`
configFile := filepath.Join(t.TempDir(), "config.yaml")
os.WriteFile(configFile, []byte(config), 0o644)
// Environment
clean := func() {
for _, env := range os.Environ() {
if strings.HasPrefix(env, "AKVORADO_CFG_DUMMY_") {
os.Unsetenv(strings.Split(env, "=")[0])
}
}
}
clean()
defer clean()
os.Setenv("AKVORADO_CFG_DUMMY_MODULE1_LISTEN", "127.0.0.1:9000")
os.Setenv("AKVORADO_CFG_DUMMY_MODULE1_TOPIC", "something")
os.Setenv("AKVORADO_CFG_DUMMY_MODULE2_DETAILS_INTERVALVALUE", "10m")
os.Setenv("AKVORADO_CFG_DUMMY_MODULE2_STUFF", "bye")
os.Setenv("AKVORADO_CFG_DUMMY_MODULE2_ELEMENTS_0_NAME", "something")
os.Setenv("AKVORADO_CFG_DUMMY_MODULE2_ELEMENTS_0_GAUGE", "18")
os.Setenv("AKVORADO_CFG_DUMMY_MODULE2_ELEMENTS_1_NAME", "something else")
os.Setenv("AKVORADO_CFG_DUMMY_MODULE2_ELEMENTS_1_GAUGE", "7")
c := cmd.ConfigRelatedOptions{
Path: configFile,
Dump: true,
}
parsed := dummyConfiguration{}
out := bytes.NewBuffer([]byte{})
if _, err := c.Parse(out, "dummy", &parsed); err != nil {
t.Fatalf("Parse() error:\n%+v", err)
}
// Expected configuration
expected := dummyConfiguration{
Module1: dummyModule1Configuration{
Listen: "127.0.0.1:9000",
Topic: "something",
Workers: 100,
},
Module2: dummyModule2Configuration{
MoreDetails: MoreDetails{
Stuff: "bye",
},
Details: dummyModule2DetailsConfiguration{
Workers: 5,
IntervalValue: 10 * time.Minute,
},
Elements: []dummyModule2ElementsConfiguration{
{"something", 18},
{"something else", 7},
},
},
}
if diff := helpers.Diff(parsed, expected); diff != "" {
t.Errorf("Parse() (-got, +want):\n%s", diff)
}
}
func TestHTTPConfiguration(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/yaml; charset=utf-8")
fmt.Fprint(w, `---
module1:
topic: flows
module2:
details:
workers: 5
interval-value: 20m
stuff: bye
elements:
- {"name": "first", "gauge": 67}
- {"name": "second"}
`)
}))
defer ts.Close()
c := cmd.ConfigRelatedOptions{
Path: ts.URL,
Dump: true,
}
parsed := dummyConfiguration{}
out := bytes.NewBuffer([]byte{})
if _, err := c.Parse(out, "dummy", &parsed); err != nil {
t.Fatalf("Parse() error:\n%+v", err)
}
// Expected configuration
expected := dummyConfiguration{
Module1: dummyModule1Configuration{
Listen: "127.0.0.1:8080",
Topic: "flows",
Workers: 100,
},
Module2: dummyModule2Configuration{
MoreDetails: MoreDetails{
Stuff: "bye",
},
Details: dummyModule2DetailsConfiguration{
Workers: 5,
IntervalValue: 20 * time.Minute,
},
Elements: []dummyModule2ElementsConfiguration{
{"first", 67},
{"second", 0},
},
},
}
if diff := helpers.Diff(parsed, expected); diff != "" {
t.Errorf("Parse() (-got, +want):\n%s", diff)
}
}
func TestUnused(t *testing.T) {
t.Run("ignored fields", func(t *testing.T) {
config := `---
.unused: should be ignored
module1:
.too: nope
topic: flow
workers: 10
`
configFile := filepath.Join(t.TempDir(), "config.yaml")
os.WriteFile(configFile, []byte(config), 0o644)
c := cmd.ConfigRelatedOptions{Path: configFile}
parsed := dummyConfiguration{}
out := bytes.NewBuffer([]byte{})
if _, err := c.Parse(out, "dummy", &parsed); err != nil {
t.Fatalf("Parse() error:\n%+v", err)
}
})
t.Run("unused fields", func(t *testing.T) {
config := `---
unused: should not be ignored
module1:
extra: 111
topic: flow
workers: 10
`
configFile := filepath.Join(t.TempDir(), "config.yaml")
os.WriteFile(configFile, []byte(config), 0o644)
c := cmd.ConfigRelatedOptions{Path: configFile}
parsed := dummyConfiguration{}
out := bytes.NewBuffer([]byte{})
if _, err := c.Parse(out, "dummy", &parsed); err == nil {
t.Fatal("Parse() didn't error")
} else if diff := helpers.Diff(err.Error(), `invalid configuration:
invalid key "Module1.extra"
invalid key "unused"`); diff != "" {
t.Fatalf("Parse() (-got, +want):\n%s", diff)
}
})
}
func TestDefaultInSlice(t *testing.T) {
try := func(t *testing.T, parse func(cmd.ConfigRelatedOptions, *bytes.Buffer) any, expected any) {
// Configuration file
config := `---
modules:
- module1:
topic: flows1
- module1:
topic: flows2
`
configFile := filepath.Join(t.TempDir(), "config.yaml")
os.WriteFile(configFile, []byte(config), 0o644)
c := cmd.ConfigRelatedOptions{
Path: configFile,
Dump: true,
}
out := bytes.NewBuffer([]byte{})
parsed := parse(c, out)
if diff := helpers.Diff(parsed, expected); diff != "" {
t.Errorf("Parse() (-got, +want):\n%s", diff)
}
}
t.Run("without pointer", func(t *testing.T) {
try(t, func(c cmd.ConfigRelatedOptions, out *bytes.Buffer) any {
parsed := struct {
Modules []dummyConfiguration
}{}
if _, err := c.Parse(out, "dummy", &parsed); err != nil {
t.Fatalf("Parse() error:\n%+v", err)
}
return parsed
},
// Expected configuration
struct {
Modules []dummyConfiguration
}{
Modules: []dummyConfiguration{
{
Module1: dummyModule1Configuration{
Listen: "127.0.0.1:8080",
Topic: "flows1",
Workers: 100,
},
Module2: dummyModule2Configuration{
MoreDetails: MoreDetails{
Stuff: "hello",
},
Details: dummyModule2DetailsConfiguration{
Workers: 1,
IntervalValue: time.Minute,
},
Elements: []dummyModule2ElementsConfiguration{
{
Name: "el1",
Gauge: 10,
}, {
Name: "el2",
Gauge: 11,
},
},
},
},
{
Module1: dummyModule1Configuration{
Listen: "127.0.0.1:8080",
Topic: "flows2",
Workers: 100,
},
Module2: dummyModule2Configuration{
MoreDetails: MoreDetails{
Stuff: "hello",
},
Details: dummyModule2DetailsConfiguration{
Workers: 1,
IntervalValue: time.Minute,
},
Elements: []dummyModule2ElementsConfiguration{
{
Name: "el1",
Gauge: 10,
}, {
Name: "el2",
Gauge: 11,
},
},
},
},
},
})
})
t.Run("with pointer", func(t *testing.T) {
try(t, func(c cmd.ConfigRelatedOptions, out *bytes.Buffer) any {
parsed := struct {
Modules []*dummyConfiguration
}{}
if _, err := c.Parse(out, "dummy", &parsed); err != nil {
t.Fatalf("Parse() error:\n%+v", err)
}
return parsed
},
struct {
Modules []*dummyConfiguration
}{
Modules: []*dummyConfiguration{
{
Module1: dummyModule1Configuration{
Listen: "127.0.0.1:8080",
Topic: "flows1",
Workers: 100,
},
Module2: dummyModule2Configuration{
MoreDetails: MoreDetails{
Stuff: "hello",
},
Details: dummyModule2DetailsConfiguration{
Workers: 1,
IntervalValue: time.Minute,
},
Elements: []dummyModule2ElementsConfiguration{
{
Name: "el1",
Gauge: 10,
}, {
Name: "el2",
Gauge: 11,
},
},
},
},
{
Module1: dummyModule1Configuration{
Listen: "127.0.0.1:8080",
Topic: "flows2",
Workers: 100,
},
Module2: dummyModule2Configuration{
MoreDetails: MoreDetails{
Stuff: "hello",
},
Details: dummyModule2DetailsConfiguration{
Workers: 1,
IntervalValue: time.Minute,
},
Elements: []dummyModule2ElementsConfiguration{
{
Name: "el1",
Gauge: 10,
}, {
Name: "el2",
Gauge: 11,
},
},
},
},
},
})
})
}
func TestDevNullDefault(t *testing.T) {
c := cmd.ConfigRelatedOptions{
Path: "/dev/null",
Dump: true,
}
var parsed dummyConfiguration
out := bytes.NewBuffer([]byte{})
if _, err := c.Parse(out, "dummy", &parsed); err != nil {
t.Fatalf("Parse() error:\n%+v", err)
}
// Expected configuration
expected := dummyConfiguration{
Module1: dummyModule1Configuration{
Listen: "127.0.0.1:8080",
Topic: "nothingness",
Workers: 100,
},
Module2: dummyModule2Configuration{
MoreDetails: MoreDetails{
Stuff: "hello",
},
Details: dummyModule2DetailsConfiguration{
Workers: 1,
IntervalValue: time.Minute,
},
Elements: []dummyModule2ElementsConfiguration{
{
Name: "el1",
Gauge: 10,
}, {
Name: "el2",
Gauge: 11,
},
},
},
}
if diff := helpers.Diff(parsed, expected); diff != "" {
t.Errorf("Parse() (-got, +want):\n%s", diff)
}
}
func TestZeroCanOverrideDefault(t *testing.T) {
config := `---
module1:
topic: flows
workers: 10
module2:
details:
workers: 0
stuff: ""
`
configFile := filepath.Join(t.TempDir(), "config.yaml")
os.WriteFile(configFile, []byte(config), 0o644)
c := cmd.ConfigRelatedOptions{
Path: configFile,
Dump: true,
}
parsed := dummyConfiguration{}
expected := dummyConfiguration{
Module1: dummyModule1Configuration{
Listen: "127.0.0.1:8080",
Topic: "flows",
Workers: 10,
},
Module2: dummyModule2Configuration{
Details: dummyModule2DetailsConfiguration{
Workers: 0,
IntervalValue: time.Minute,
},
MoreDetails: MoreDetails{
Stuff: "",
},
Elements: []dummyModule2ElementsConfiguration{
{
Name: "el1",
Gauge: 10,
}, {
Name: "el2",
Gauge: 11,
},
},
},
}
out := bytes.NewBuffer([]byte{})
if _, err := c.Parse(out, "dummy", &parsed); err != nil {
t.Fatalf("Parse() error:\n%+v", err)
}
if diff := helpers.Diff(parsed, expected); diff != "" {
t.Fatalf("Parse() (-got, +want):\n%s", diff)
}
log.Printf("output:\n%s", out.Bytes())
if !bytes.Contains(out.Bytes(), []byte(`stuff: ""`)) {
t.Errorf("Dumped configuration should contain `stuff: \"\"`")
}
if !bytes.Contains(out.Bytes(), []byte(`workers: 0`)) {
t.Errorf("Dumped configuration should contain `workers: 0`")
}
}