diff options
Diffstat (limited to 'src/go/plugin/go.d/modules/tor/tor_test.go')
-rw-r--r-- | src/go/plugin/go.d/modules/tor/tor_test.go | 328 |
1 files changed, 328 insertions, 0 deletions
diff --git a/src/go/plugin/go.d/modules/tor/tor_test.go b/src/go/plugin/go.d/modules/tor/tor_test.go new file mode 100644 index 000000000..35001c39a --- /dev/null +++ b/src/go/plugin/go.d/modules/tor/tor_test.go @@ -0,0 +1,328 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package tor + +import ( + "bufio" + "errors" + "fmt" + "io" + "net" + "os" + "strings" + "testing" + "time" + + "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + dataConfigJSON, _ = os.ReadFile("testdata/config.json") + dataConfigYAML, _ = os.ReadFile("testdata/config.yaml") +) + +func Test_testDataIsValid(t *testing.T) { + for name, data := range map[string][]byte{ + "dataConfigJSON": dataConfigJSON, + "dataConfigYAML": dataConfigYAML, + } { + require.NotNil(t, data, name) + } +} + +func TestTor_ConfigurationSerialize(t *testing.T) { + module.TestConfigurationSerialize(t, &Tor{}, dataConfigJSON, dataConfigYAML) +} + +func TestTor_Init(t *testing.T) { + tests := map[string]struct { + config Config + wantFail bool + }{ + "success with default config": { + wantFail: false, + config: New().Config, + }, + "fails if address not set": { + wantFail: true, + config: func() Config { + conf := New().Config + conf.Address = "" + return conf + }(), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + tor := New() + tor.Config = test.config + + if test.wantFail { + assert.Error(t, tor.Init()) + } else { + assert.NoError(t, tor.Init()) + } + }) + } +} + +func TestTor_Charts(t *testing.T) { + assert.NotNil(t, New().Charts()) +} + +func TestTor_Check(t *testing.T) { + tests := map[string]struct { + prepare func() (*Tor, *mockTorDaemon) + wantFail bool + }{ + "success on valid response": { + wantFail: false, + prepare: prepareCaseOk, + }, + "fails on connection refused": { + wantFail: true, + prepare: prepareCaseConnectionRefused, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + tor, daemon := test.prepare() + + defer func() { + assert.NoError(t, daemon.Close(), "daemon.Close()") + }() + go func() { + assert.NoError(t, daemon.Run(), "daemon.Run()") + }() + + select { + case <-daemon.started: + case <-time.After(time.Second * 3): + t.Errorf("mock tor daemon start timed out") + } + + require.NoError(t, tor.Init()) + + if test.wantFail { + assert.Error(t, tor.Check()) + } else { + assert.NoError(t, tor.Check()) + } + + tor.Cleanup() + + select { + case <-daemon.stopped: + case <-time.After(time.Second * 3): + t.Errorf("mock tor daemon stop timed out") + } + }) + } +} + +func TestTor_Collect(t *testing.T) { + tests := map[string]struct { + prepare func() (*Tor, *mockTorDaemon) + wantMetrics map[string]int64 + wantCharts int + }{ + "success on valid response": { + prepare: prepareCaseOk, + wantCharts: len(charts), + wantMetrics: map[string]int64{ + "traffic/read": 100, + "traffic/written": 100, + "uptime": 100, + }, + }, + "fails on connection refused": { + prepare: prepareCaseConnectionRefused, + wantCharts: len(charts), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + tor, daemon := test.prepare() + + defer func() { + assert.NoError(t, daemon.Close(), "daemon.Close()") + }() + go func() { + assert.NoError(t, daemon.Run(), "daemon.Run()") + }() + + select { + case <-daemon.started: + case <-time.After(time.Second * 3): + t.Errorf("mock tor daemon start timed out") + } + + require.NoError(t, tor.Init()) + + mx := tor.Collect() + + require.Equal(t, test.wantMetrics, mx) + + assert.Equal(t, test.wantCharts, len(*tor.Charts()), "want charts") + + if len(test.wantMetrics) > 0 { + module.TestMetricsHasAllChartsDims(t, tor.Charts(), mx) + } + + tor.Cleanup() + + select { + case <-daemon.stopped: + case <-time.After(time.Second * 3): + t.Errorf("mock tordaemon stop timed out") + } + }) + } +} + +func prepareCaseOk() (*Tor, *mockTorDaemon) { + daemon := &mockTorDaemon{ + addr: "127.0.0.1:65001", + started: make(chan struct{}), + stopped: make(chan struct{}), + } + + tor := New() + tor.Address = daemon.addr + + return tor, daemon +} + +func prepareCaseConnectionRefused() (*Tor, *mockTorDaemon) { + ch := make(chan struct{}) + close(ch) + + daemon := &mockTorDaemon{ + addr: "127.0.0.1:65001", + dontStart: true, + started: ch, + stopped: ch, + } + + tor := New() + tor.Address = daemon.addr + + return tor, daemon +} + +type mockTorDaemon struct { + addr string + srv net.Listener + started chan struct{} + stopped chan struct{} + dontStart bool + authenticated bool +} + +func (m *mockTorDaemon) Run() error { + if m.dontStart { + return nil + } + + srv, err := net.Listen("tcp", m.addr) + if err != nil { + return err + } + + m.srv = srv + + close(m.started) + defer close(m.stopped) + + return m.handleConnections() +} + +func (m *mockTorDaemon) Close() error { + if m.srv != nil { + err := m.srv.Close() + m.srv = nil + return err + } + return nil +} + +func (m *mockTorDaemon) handleConnections() error { + conn, err := m.srv.Accept() + if err != nil || conn == nil { + return errors.New("could not accept connection") + } + return m.handleConnection(conn) +} + +func (m *mockTorDaemon) handleConnection(conn net.Conn) error { + defer func() { _ = conn.Close() }() + + rw := bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)) + var line string + var err error + + for { + if line, err = rw.ReadString('\n'); err != nil { + return fmt.Errorf("error reading from connection: %v", err) + } + + line = strings.TrimSpace(line) + + cmd, param, _ := strings.Cut(line, " ") + + switch cmd { + case cmdQuit: + return m.handleQuit(conn) + case cmdAuthenticate: + err = m.handleAuthenticate(conn) + case cmdGetInfo: + err = m.handleGetInfo(conn, param) + default: + s := fmt.Sprintf("510 Unrecognized command \"%s\"\n", cmd) + _, _ = rw.WriteString(s) + return fmt.Errorf("unexpected command: %s", line) + } + + _ = rw.Flush() + + if err != nil { + return err + } + } +} + +func (m *mockTorDaemon) handleQuit(conn io.Writer) error { + _, err := conn.Write([]byte("250 closing connection\n")) + return err +} + +func (m *mockTorDaemon) handleAuthenticate(conn io.Writer) error { + m.authenticated = true + _, err := conn.Write([]byte("250 OK\n")) + return err +} + +func (m *mockTorDaemon) handleGetInfo(conn io.Writer, keywords string) error { + if !m.authenticated { + _, _ = conn.Write([]byte("514 Authentication required\n")) + return errors.New("authentication required") + } + + keywords = strings.Trim(keywords, "\"") + + for _, k := range strings.Fields(keywords) { + s := fmt.Sprintf("250-%s=%d\n", k, 100) + + if _, err := conn.Write([]byte(s)); err != nil { + return err + } + } + + _, err := conn.Write([]byte("250 OK\n")) + return err +} |