add: test cases for rcon

This commit is contained in:
2026-02-13 14:14:08 -08:00
parent 7c1697660f
commit db8304bebd
6 changed files with 147 additions and 73 deletions

5
go.mod
View File

@@ -2,4 +2,7 @@ module tipsy.codes/charles/mc-god/v2
go 1.25.6 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
)

2
go.sum
View File

@@ -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 h1:pYwZ8Rhcgfh/LhdPBncecuEo5thoFvPIuMSWovz1FME=
github.com/gorcon/rcon v1.4.0/go.mod h1:M6v6sNmr/NET9YIf+2rq+cIjTBridoy62uzQ58WgC1I= github.com/gorcon/rcon v1.4.0/go.mod h1:M6v6sNmr/NET9YIf+2rq+cIjTBridoy62uzQ58WgC1I=

View File

@@ -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_ADDRESS` - The address of the Minecraft server (e.g., "localhost:25575")
- `RCON_PASSWORD` - The RCON password for authentication - `RCON_PASSWORD` - The RCON password for authentication
## Methods ## Testing
- `New(address, password)` - Create a new RCON client `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.
- `Execute(command)` - Execute a command on the server
- `SetWeather(weather)` - Set the weather (clear, rain, thunder) The protocol is described at https://developer.valvesoftware.com/wiki/Source_RCON_Protocol.
- `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

View File

@@ -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)
}

View File

@@ -56,61 +56,17 @@ func (c *Client) Ping() error {
// SetWeather sets the weather in the Minecraft world // SetWeather sets the weather in the Minecraft world
func (c *Client) SetWeather(weather string) error { func (c *Client) SetWeather(weather string) error {
var command string return fmt.Errorf("not implemented")
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
} }
// SetTime sets the time in the Minecraft world // SetTime sets the time in the Minecraft world
func (c *Client) SetTime(timeValue string) error { func (c *Client) SetTime(timeValue string) error {
var command string return fmt.Errorf("not implemented")
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
} }
// SetDifficulty sets the difficulty level // SetDifficulty sets the difficulty level
func (c *Client) SetDifficulty(difficulty string) error { func (c *Client) SetDifficulty(difficulty string) error {
var command string return fmt.Errorf("not implemented")
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
} }
// GetServerInfo returns basic server information // GetServerInfo returns basic server information
@@ -118,14 +74,6 @@ func (c *Client) GetServerInfo() (string, error) {
return c.Execute("version") 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 // ConnectWithTimeout attempts to connect with a timeout
func ConnectWithTimeout(address, password string, timeout time.Duration) (*Client, error) { func ConnectWithTimeout(address, password string, timeout time.Duration) (*Client, error) {
ctx, cancel := context.WithTimeout(context.Background(), timeout) ctx, cancel := context.WithTimeout(context.Background(), timeout)

View File

@@ -2,13 +2,34 @@ package rcon
import ( import (
"testing" "testing"
"github.com/google/go-cmp/cmp"
) )
func TestNew(t *testing.T) { func TestNew(t *testing.T) {
// This is a placeholder test since we can't actually connect to a server serv := MockRCONServer{}
// in a test environment without a real Minecraft server done, err := serv.Start()
// The actual functionality will be tested with integration tests if err != nil {
t.Log("RCON package tests - placeholder for actual tests") 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) { func TestGetEnvCredentials(t *testing.T) {