From db8304bebd19a9b841357e8c97c8c45e9e9169ab Mon Sep 17 00:00:00 2001 From: charles Date: Fri, 13 Feb 2026 14:14:08 -0800 Subject: [PATCH] add: test cases for rcon --- go.mod | 5 +- go.sum | 2 + internal/pkg/rcon/README.md | 17 ++--- internal/pkg/rcon/mockrcon.go | 109 +++++++++++++++++++++++++++++++++ internal/pkg/rcon/rcon.go | 58 +----------------- internal/pkg/rcon/rcon_test.go | 29 +++++++-- 6 files changed, 147 insertions(+), 73 deletions(-) create mode 100644 internal/pkg/rcon/mockrcon.go diff --git a/go.mod b/go.mod index adb9350..99b934c 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module tipsy.codes/charles/mc-god/v2 go 1.25.6 -require github.com/gorcon/rcon v1.4.0 +require ( + github.com/google/go-cmp v0.7.0 + github.com/gorcon/rcon v1.4.0 +) diff --git a/go.sum b/go.sum index 4bcf23d..4baadc7 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,4 @@ +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/gorcon/rcon v1.4.0 h1:pYwZ8Rhcgfh/LhdPBncecuEo5thoFvPIuMSWovz1FME= github.com/gorcon/rcon v1.4.0/go.mod h1:M6v6sNmr/NET9YIf+2rq+cIjTBridoy62uzQ58WgC1I= diff --git a/internal/pkg/rcon/README.md b/internal/pkg/rcon/README.md index cc94524..9aa3d0b 100644 --- a/internal/pkg/rcon/README.md +++ b/internal/pkg/rcon/README.md @@ -44,17 +44,8 @@ The package expects these environment variables to be set: - `RCON_ADDRESS` - The address of the Minecraft server (e.g., "localhost:25575") - `RCON_PASSWORD` - The RCON password for authentication -## Methods +## Testing -- `New(address, password)` - Create a new RCON client -- `Execute(command)` - Execute a command on the server -- `SetWeather(weather)` - Set the weather (clear, rain, thunder) -- `SetTime(timeValue)` - Set the time (day, night, noon, midnight, or numeric) -- `SetDifficulty(difficulty)` - Set the difficulty level (peaceful, easy, normal, hard) -- `GetServerInfo()` - Get server version information -- `Close()` - Close the RCON connection -- `HealthCheck()` - Verify the connection is working -- `ExecuteWithTimeout()` - Execute command with timeout -- `ConnectWithTimeout()` - Connect with timeout -- `GetEnvCredentials()` - Get credentials from environment -- `NewFromEnv()` - Create client from environment variables \ No newline at end of file +`rconmock.go` contains a mock rcon server that accepts and logs requests from the user. It can be configured to return errors, or success (empty body strings). It logs the recieved message bodies. + +The protocol is described at https://developer.valvesoftware.com/wiki/Source_RCON_Protocol. diff --git a/internal/pkg/rcon/mockrcon.go b/internal/pkg/rcon/mockrcon.go new file mode 100644 index 0000000..d6ab641 --- /dev/null +++ b/internal/pkg/rcon/mockrcon.go @@ -0,0 +1,109 @@ +package rcon + +import ( + "fmt" + "net" + "sync" + + "github.com/gorcon/rcon" +) + +// MockRCONServer simulates an RCON server for testing purposes +type MockRCONServer struct { + listener net.Listener + Commands []string + Errors []error + Messages []string +} + +// Start starts the mock RCON server on a random available port +func (m *MockRCONServer) Start() (func() *RCONResults, error) { + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return nil, fmt.Errorf("failed to start mock server: %w", err) + } + m.listener = listener + + results := &RCONResults{} + done := sync.WaitGroup{} + + done.Go(func() { + for { + conn, err := listener.Accept() + if err != nil { + return + } + go m.handleConnection(conn, results) + } + }) + + return func() *RCONResults { + m.stop() + done.Wait() + return results + }, nil +} + +// Stop stops the mock RCON server +func (m *MockRCONServer) stop() error { + if m.listener != nil { + return m.listener.Close() + } + return nil +} + +// Address returns the address the mock server is listening on +func (m *MockRCONServer) Address() string { + if m.listener != nil { + return m.listener.Addr().String() + } + return "" +} + +// handleConnection handles individual client connections +func (m *MockRCONServer) handleConnection(conn net.Conn, results *RCONResults) { + defer conn.Close() + + for { + packet := &rcon.Packet{} + if _, err := packet.ReadFrom(conn); err != nil { + results.AppendError(err) + return + } + + results.AppendMessage(packet.Body()) + + var respPacket *rcon.Packet + switch packet.Type { + case rcon.SERVERDATA_AUTH: + respPacket = rcon.NewPacket(rcon.SERVERDATA_AUTH_RESPONSE, packet.ID, "") + case rcon.SERVERDATA_EXECCOMMAND: + respPacket = rcon.NewPacket(rcon.SERVERDATA_RESPONSE_VALUE, packet.ID, "") + default: + results.AppendError(fmt.Errorf("unknown packet: %v", packet.Type)) + return + } + if _, err := respPacket.WriteTo(conn); err != nil { + results.AppendError(err) + return + } + } +} + +type RCONResults struct { + mu sync.Mutex + Messages []string + Errors []error +} + +func (r *RCONResults) AppendError(err error) { + r.mu.Lock() + defer r.mu.Unlock() + r.Errors = append(r.Errors, err) +} + +func (r *RCONResults) AppendMessage(msg string) { + r.mu.Lock() + defer r.mu.Unlock() + r.Messages = append(r.Messages, msg) +} diff --git a/internal/pkg/rcon/rcon.go b/internal/pkg/rcon/rcon.go index c687050..ecf7f84 100644 --- a/internal/pkg/rcon/rcon.go +++ b/internal/pkg/rcon/rcon.go @@ -56,61 +56,17 @@ func (c *Client) Ping() error { // SetWeather sets the weather in the Minecraft world func (c *Client) SetWeather(weather string) error { - var command string - switch weather { - case "clear": - command = "weather clear" - case "rain": - command = "weather rain" - case "thunder": - command = "weather thunder" - default: - return fmt.Errorf("invalid weather type: %s", weather) - } - - _, err := c.Execute(command) - return err + return fmt.Errorf("not implemented") } // SetTime sets the time in the Minecraft world func (c *Client) SetTime(timeValue string) error { - var command string - switch timeValue { - case "day": - command = "time set day" - case "night": - command = "time set night" - case "noon": - command = "time set noon" - case "midnight": - command = "time set midnight" - default: - // Assume it's a numeric value - command = fmt.Sprintf("time set %s", timeValue) - } - - _, err := c.Execute(command) - return err + return fmt.Errorf("not implemented") } // SetDifficulty sets the difficulty level func (c *Client) SetDifficulty(difficulty string) error { - var command string - switch difficulty { - case "peaceful": - command = "difficulty peaceful" - case "easy": - command = "difficulty easy" - case "normal": - command = "difficulty normal" - case "hard": - command = "difficulty hard" - default: - return fmt.Errorf("invalid difficulty level: %s", difficulty) - } - - _, err := c.Execute(command) - return err + return fmt.Errorf("not implemented") } // GetServerInfo returns basic server information @@ -118,14 +74,6 @@ func (c *Client) GetServerInfo() (string, error) { return c.Execute("version") } -// TailLogs starts tailing the server logs -func (c *Client) TailLogs(ctx context.Context, handler func(string)) error { - // This is a placeholder implementation - // In a real implementation, this would need to be connected to actual log tailing - // For now, we'll just return an error to indicate this is not implemented - return fmt.Errorf("log tailing not implemented in this version") -} - // ConnectWithTimeout attempts to connect with a timeout func ConnectWithTimeout(address, password string, timeout time.Duration) (*Client, error) { ctx, cancel := context.WithTimeout(context.Background(), timeout) diff --git a/internal/pkg/rcon/rcon_test.go b/internal/pkg/rcon/rcon_test.go index d43d1e2..dbca7fc 100644 --- a/internal/pkg/rcon/rcon_test.go +++ b/internal/pkg/rcon/rcon_test.go @@ -2,13 +2,34 @@ package rcon import ( "testing" + + "github.com/google/go-cmp/cmp" ) func TestNew(t *testing.T) { - // This is a placeholder test since we can't actually connect to a server - // in a test environment without a real Minecraft server - // The actual functionality will be tested with integration tests - t.Log("RCON package tests - placeholder for actual tests") + serv := MockRCONServer{} + done, err := serv.Start() + if err != nil { + t.Fatalf("failed to start server: %v", err) + } + + password := "abc123" + client, err := New(serv.Address(), password) + if err != nil { + t.Fatalf("failed to connect: %v", err) + } + if _, err = client.Execute("Hello world!"); err != nil { + t.Fatalf("failed to run command: %v", err) + } + + results := done() + want := []string{ + "abc123", + "Hello world!", + } + if diff := cmp.Diff(want, results.Messages); diff != "" { + t.Errorf("Got diff (-want +got):\n%s", diff) + } } func TestGetEnvCredentials(t *testing.T) {