From 21cae177d982623aa590de8d86b1dd5fabe0a1e3 Mon Sep 17 00:00:00 2001 From: sbruens Date: Tue, 10 Dec 2024 13:09:23 -0500 Subject: [PATCH] Update tests for config validation. --- cmd/outline-ss-server/config.go | 15 +- cmd/outline-ss-server/config_test.go | 419 +++++++++++++++++++-------- 2 files changed, 311 insertions(+), 123 deletions(-) diff --git a/cmd/outline-ss-server/config.go b/cmd/outline-ss-server/config.go index 6c22efd7..f677d198 100644 --- a/cmd/outline-ss-server/config.go +++ b/cmd/outline-ss-server/config.go @@ -58,10 +58,12 @@ type LegacyKeyServiceConfig struct { Port int `yaml:"port"` } +type WebConfig struct { + Servers []WebServerConfig `yaml:"servers"` +} + type Config struct { - Web struct { - Servers []WebServerConfig `yaml:"servers"` - } `yaml:"web"` + Web WebConfig `yaml:"web"` Services []ServiceConfig `yaml:"services"` // Deprecated: `keys` exists for backward compatibility. Prefer to configure @@ -103,14 +105,17 @@ func (c *Config) Validate() error { } case listenerTypeWebsocketStream, listenerTypeWebsocketPacket: if lnConfig.WebServer == "" { - return fmt.Errorf("listener type `%s` requires an http server reference", lnConfig.Type) + return fmt.Errorf("listener type `%s` requires a `web_server`", lnConfig.Type) + } + if lnConfig.Path == "" { + return fmt.Errorf("listener type `%s` requires a `path`", lnConfig.Type) } if _, exists := existingWebServers[lnConfig.WebServer]; !exists { return fmt.Errorf("listener type `%s` references unknown web server `%s`", lnConfig.Type, lnConfig.WebServer) } key = fmt.Sprintf("%s/%s", lnConfig.Type, lnConfig.WebServer) if _, exists := existingListeners[key]; exists { - return fmt.Errorf("listener of type `%s` with http server `%s` already exists.", lnConfig.Type, lnConfig.WebServer) + return fmt.Errorf("listener of type `%s` with web server `%s` already exists.", lnConfig.Type, lnConfig.WebServer) } default: return fmt.Errorf("unsupported listener type: %s", lnConfig.Type) diff --git a/cmd/outline-ss-server/config_test.go b/cmd/outline-ss-server/config_test.go index ce40e249..7c71cf08 100644 --- a/cmd/outline-ss-server/config_test.go +++ b/cmd/outline-ss-server/config_test.go @@ -16,178 +16,361 @@ package main import ( "os" + "strings" "testing" "github.com/stretchr/testify/require" ) -func TestValidateConfigFails(t *testing.T) { - tests := []struct { - name string - cfg *Config - }{ - { - name: "WithUnknownListenerType", - cfg: &Config{ - Services: []ServiceConfig{ - ServiceConfig{ - Listeners: []ListenerConfig{ - ListenerConfig{Type: "foo", Address: "[::]:9000"}, +func TestConfigValidate(t *testing.T) { + t.Run("InvalidConfig", func(t *testing.T) { + tests := []struct { + name string + cfg *Config + errStr string + }{ + { + name: "UnknownListenerType", + cfg: &Config{ + Services: []ServiceConfig{ + ServiceConfig{ + Listeners: []ListenerConfig{ + ListenerConfig{Type: "foo", Address: "[::]:9000"}, + }, }, }, }, + errStr: "unsupported listener type", }, - }, - { - name: "WithInvalidListenerAddress", - cfg: &Config{ - Services: []ServiceConfig{ - ServiceConfig{ - Listeners: []ListenerConfig{ - ListenerConfig{Type: listenerTypeTCP, Address: "tcp/[::]:9000"}, + { + name: "InvalidListenerAddress", + cfg: &Config{ + Services: []ServiceConfig{ + ServiceConfig{ + Listeners: []ListenerConfig{ + ListenerConfig{Type: listenerTypeTCP, Address: "tcp/[::]:9000"}, + }, }, }, }, + errStr: "invalid listener address", }, - }, - { - name: "WithHostnameAddress", - cfg: &Config{ - Services: []ServiceConfig{ - ServiceConfig{ - Listeners: []ListenerConfig{ - ListenerConfig{Type: listenerTypeTCP, Address: "example.com:9000"}, + { + name: "HostnameAddress", + cfg: &Config{ + Services: []ServiceConfig{ + ServiceConfig{ + Listeners: []ListenerConfig{ + ListenerConfig{Type: listenerTypeTCP, Address: "example.com:9000"}, + }, }, }, }, + errStr: "address must be IP", }, - }, - { - name: "WithDuplicateListeners", - cfg: &Config{ - Services: []ServiceConfig{ - ServiceConfig{ - Listeners: []ListenerConfig{ - ListenerConfig{Type: listenerTypeTCP, Address: "[::]:9000"}, + { + name: "DuplicateListeners", + cfg: &Config{ + Services: []ServiceConfig{ + ServiceConfig{ + Listeners: []ListenerConfig{ + ListenerConfig{Type: listenerTypeTCP, Address: "[::]:9000"}, + }, + }, + ServiceConfig{ + Listeners: []ListenerConfig{ + ListenerConfig{Type: listenerTypeTCP, Address: "[::]:9000"}, + }, }, }, - ServiceConfig{ - Listeners: []ListenerConfig{ - ListenerConfig{Type: listenerTypeTCP, Address: "[::]:9000"}, + }, + errStr: "already exists", + }, + { + name: "WebServerMissingID", + cfg: &Config{ + Web: WebConfig{ + Servers: []WebServerConfig{ + { + Listeners: []string{"[::]:8000"}, + }, }, }, + Services: []ServiceConfig{}, }, + errStr: "web server must have an ID", }, - }, - { - name: "WithWebSocketWithoutOptions", - cfg: &Config{ - Services: []ServiceConfig{ - ServiceConfig{ - Listeners: []ListenerConfig{ - ListenerConfig{Type: listenerTypeWebSocket, Address: "[::]:9000"}, + { + name: "WebServerDuplicateID", + cfg: &Config{ + Web: WebConfig{ + Servers: []WebServerConfig{ + { + ID: "foo", + Listeners: []string{"[::]:8000"}, + }, + { + ID: "foo", + Listeners: []string{"[::]:8001"}, + }, }, }, + Services: []ServiceConfig{}, }, + errStr: "already exists", }, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - err := tc.cfg.Validate() - require.Error(t, err) - }) - } -} - -func TestReadConfig(t *testing.T) { - config, err := readConfigFile("./config_example.yml") - - require.NoError(t, err) - expected := Config{ - Services: []ServiceConfig{ - ServiceConfig{ - Listeners: []ListenerConfig{ - ListenerConfig{Type: listenerTypeTCP, Address: "[::]:9000"}, - ListenerConfig{Type: listenerTypeUDP, Address: "[::]:9000"}, - ListenerConfig{ - Type: listenerTypeWebSocket, - Address: "[::]:8000", - Options: []ConfigOption{ + { + name: "WebServerInvalidAddress", + cfg: &Config{ + Web: WebConfig{ + Servers: []WebServerConfig{ { - Path: "/tcp", - ConnectionType: connectionTypeStream, + ID: "foo", + Listeners: []string{":invalid"}, }, + }, + }, + Services: []ServiceConfig{}, + }, + errStr: "invalid listener for web server `foo`", + }, + { + name: "WebsocketListenerMissingWebServer", + cfg: &Config{ + Web: WebConfig{ + Servers: []WebServerConfig{ + { + ID: "foo", + Listeners: []string{"[::]:8000"}, + }, + }, + }, + Services: []ServiceConfig{ + { + Listeners: []ListenerConfig{ + { + Type: listenerTypeWebsocketStream, + Path: "/tcp", + }, + }, + }, + }, + }, + errStr: "requires a `web_server`", + }, + { + name: "WebsocketListenerUnknownWebServer", + cfg: &Config{ + Web: WebConfig{ + Servers: []WebServerConfig{ + { + ID: "foo", + Listeners: []string{"[::]:8000"}, + }, + }, + }, + Services: []ServiceConfig{ + { + Listeners: []ListenerConfig{ + { + Type: listenerTypeWebsocketStream, + WebServer: "unknown_server", + Path: "/tcp", + }, + }, + }, + }, + }, + errStr: "unknown web server `unknown_server`", + }, + { + name: "WebsocketListenerMissingPath", + cfg: &Config{ + Web: WebConfig{ + Servers: []WebServerConfig{ { - Path: "/udp", - ConnectionType: connectionTypePacket, + ID: "foo", + Listeners: []string{"[::]:8000"}, + }, + }, + }, + Services: []ServiceConfig{ + { + Listeners: []ListenerConfig{ + { + Type: listenerTypeWebsocketStream, + WebServer: "foo", + }, }, }, }, }, - Keys: []KeyConfig{ - KeyConfig{"user-0", "chacha20-ietf-poly1305", "Secret0"}, - KeyConfig{"user-1", "chacha20-ietf-poly1305", "Secret1"}, + errStr: "requires a `path`", + }, + { + name: "ListenerInvalidType", + cfg: &Config{ + Web: WebConfig{ + Servers: []WebServerConfig{ + { + ID: "foo", + Listeners: []string{"[::]:8000"}, + }, + }, + }, + Services: []ServiceConfig{ + { + Listeners: []ListenerConfig{ + { + Type: "invalid-type", + WebServer: "foo", + Path: "/tcp", + }, + }, + }, + }, }, + errStr: "unsupported listener type: invalid-type", }, - ServiceConfig{ - Listeners: []ListenerConfig{ - ListenerConfig{Type: listenerTypeTCP, Address: "[::]:9001"}, - ListenerConfig{Type: listenerTypeUDP, Address: "[::]:9001"}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := tc.cfg.Validate() + require.Error(t, err) + if !isStrInError(err, tc.errStr) { + t.Errorf("Config.Validate() error=`%v`, expected=`%v`", err, tc.errStr) + } + }) + } + }) + + t.Run("ValidConfig", func(t *testing.T) { + config := Config{ + Web: WebConfig{ + Servers: []WebServerConfig{ + { + ID: "my_web_server", + Listeners: []string{"[::]:8000"}, + }, }, - Keys: []KeyConfig{ - KeyConfig{"user-2", "chacha20-ietf-poly1305", "Secret2"}, + }, + Services: []ServiceConfig{ + { + Listeners: []ListenerConfig{ + { + Type: listenerTypeWebsocketStream, + WebServer: "my_web_server", + Path: "/tcp", + }, + { + Type: listenerTypeWebsocketPacket, + WebServer: "my_web_server", + Path: "/udp", + }, + }, + Keys: []KeyConfig{ + { + ID: "user-0", + Cipher: "chacha20-ietf-poly1305", + Secret: "Secret0", + }, + }, }, }, - }, - } - require.Equal(t, expected, *config) + } + err := config.Validate() + require.NoError(t, err) + }) } -func TestReadConfigParsesDeprecatedFormat(t *testing.T) { - config, err := readConfigFile("./config_example.deprecated.yml") +func TestReadConfig(t *testing.T) { + + t.Run("ExampleFile", func(t *testing.T) { + config, err := readConfigFile("./config_example.yml") - require.NoError(t, err) - expected := Config{ - Keys: []LegacyKeyServiceConfig{ - LegacyKeyServiceConfig{ - KeyConfig: KeyConfig{ID: "user-0", Cipher: "chacha20-ietf-poly1305", Secret: "Secret0"}, - Port: 9000, + require.NoError(t, err) + expected := Config{ + Web: WebConfig{ + Servers: []WebServerConfig{ + WebServerConfig{ID: "my_web_server", Listeners: []string{"[::]:8000"}}, + }, }, - LegacyKeyServiceConfig{ - KeyConfig: KeyConfig{ID: "user-1", Cipher: "chacha20-ietf-poly1305", Secret: "Secret1"}, - Port: 9000, + Services: []ServiceConfig{ + ServiceConfig{ + Listeners: []ListenerConfig{ + ListenerConfig{Type: listenerTypeTCP, Address: "[::]:9000"}, + ListenerConfig{Type: listenerTypeUDP, Address: "[::]:9000"}, + ListenerConfig{Type: listenerTypeWebsocketStream, WebServer: "my_web_server", Path: "/tcp"}, + ListenerConfig{Type: listenerTypeWebsocketPacket, WebServer: "my_web_server", Path: "/udp"}, + }, + Keys: []KeyConfig{ + KeyConfig{"user-0", "chacha20-ietf-poly1305", "Secret0"}, + KeyConfig{"user-1", "chacha20-ietf-poly1305", "Secret1"}, + }, + }, + ServiceConfig{ + Listeners: []ListenerConfig{ + ListenerConfig{Type: listenerTypeTCP, Address: "[::]:9001"}, + ListenerConfig{Type: listenerTypeUDP, Address: "[::]:9001"}, + }, + Keys: []KeyConfig{ + KeyConfig{"user-2", "chacha20-ietf-poly1305", "Secret2"}, + }, + }, }, - LegacyKeyServiceConfig{ - KeyConfig: KeyConfig{ID: "user-2", Cipher: "chacha20-ietf-poly1305", Secret: "Secret2"}, - Port: 9001, + } + require.Equal(t, expected, *config) + }) + + t.Run("ParsesDeprecatedFormat", func(t *testing.T) { + config, err := readConfigFile("./config_example.deprecated.yml") + + require.NoError(t, err) + expected := Config{ + Keys: []LegacyKeyServiceConfig{ + LegacyKeyServiceConfig{ + KeyConfig: KeyConfig{ID: "user-0", Cipher: "chacha20-ietf-poly1305", Secret: "Secret0"}, + Port: 9000, + }, + LegacyKeyServiceConfig{ + KeyConfig: KeyConfig{ID: "user-1", Cipher: "chacha20-ietf-poly1305", Secret: "Secret1"}, + Port: 9000, + }, + LegacyKeyServiceConfig{ + KeyConfig: KeyConfig{ID: "user-2", Cipher: "chacha20-ietf-poly1305", Secret: "Secret2"}, + Port: 9001, + }, }, - }, - } - require.Equal(t, expected, *config) -} + } + require.Equal(t, expected, *config) + }) -func TestReadConfigFromEmptyFile(t *testing.T) { - file, _ := os.CreateTemp("", "empty.yaml") + t.Run("FromEmptyFile", func(t *testing.T) { + file, _ := os.CreateTemp("", "empty.yaml") - config, err := readConfigFile(file.Name()) + config, err := readConfigFile(file.Name()) - require.NoError(t, err) - require.ElementsMatch(t, Config{}, config) -} + require.NoError(t, err) + require.ElementsMatch(t, Config{}, config) + }) -func TestReadConfigFromIncorrectFormatFails(t *testing.T) { - file, _ := os.CreateTemp("", "empty.yaml") - file.WriteString("foo") + t.Run("FromIncorrectFormatFails", func(t *testing.T) { + file, _ := os.CreateTemp("", "empty.yaml") + file.WriteString("foo") - config, err := readConfigFile(file.Name()) + config, err := readConfigFile(file.Name()) - require.Error(t, err) - require.ElementsMatch(t, Config{}, config) + require.Error(t, err) + require.ElementsMatch(t, Config{}, config) + }) } func readConfigFile(filename string) (*Config, error) { configData, _ := os.ReadFile(filename) return readConfig(configData) } + +func isStrInError(err error, str string) bool { + return err != nil && strings.Contains(err.Error(), str) +}