Files
VeillaxandClaude Sonnet 5.5 0d2cb31abf
CI / test (push) Successful in 2m26s
Release / release (push) Successful in 2m36s
Megalodon: manage ARK servers over RCON from a web dashboard
Watches any number of ARK:SE servers (RCON for players and chat, the query
port for map, version and player cap), records joins, leaves, chat and an
audit trail, and keeps a player database across servers.

Control: save, broadcast, chat, whisper, kick, time of day, MOTD, wild dino
wipe, a raw console with history and macros, restarts with timed warnings and
a world save, optional start command and auto-start. Bans are kept by
Megalodon: timed, cluster-wide and re-enforced when the player returns.

Automation: schedules (every N or at times of day), Discord/ntfy/webhook
notifications, welcome message, chat word filter.

Access: viewer, moderator and admin roles, API tokens, Prometheus metrics.
cmd/fakeark simulates a server for trying it out.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 23:36:13 -06:00

108 lines
3.2 KiB
Go

package ark
import (
"net"
"testing"
"time"
)
func TestParsePlayers(t *testing.T) {
got := ParsePlayers("0. Alex, the Tamer, 76561198000000001\r\n1. Bo, 76561198000000002 \n")
if len(got) != 2 || got[0].Name != "Alex, the Tamer" || got[0].SteamID != "76561198000000001" || got[1].Name != "Bo" {
t.Fatalf("got %+v", got)
}
if n := len(ParsePlayers("No Players Connected \n")); n != 0 {
t.Fatalf("expected nobody, got %d", n)
}
}
func TestParseChat(t *testing.T) {
got := ParseChat("Alex (Rex Rider): hello: world\nSERVER: restarting soon\nsomething odd\n" + NoResponse + "\n")
if len(got) != 3 {
t.Fatalf("got %+v", got)
}
if a := got[0]; a.Sender != "Alex" || a.Character != "Rex Rider" || a.Message != "hello: world" || a.Server {
t.Fatalf("player line: %+v", a)
}
if !got[1].Server || got[1].Message != "restarting soon" {
t.Fatalf("server line: %+v", got[1])
}
if !got[2].Server || got[2].Message != "something odd" {
t.Fatalf("odd line: %+v", got[2])
}
if n := len(ParseChat("[2026.01.02-03.04.05] Bo: hi")); n != 1 {
t.Fatalf("timestamped line not parsed")
}
}
func TestCommandsAreSafe(t *testing.T) {
if _, err := ForPlayer("KickPlayer", "123; DoExit"); err == nil {
t.Fatal("a non-numeric id was accepted")
}
c, err := ForPlayer("KickPlayer", "76561198000000001")
if err != nil || c != "KickPlayer 76561198000000001" {
t.Fatalf("got %q, %v", c, err)
}
c, err = Broadcast("hi \"there\"\nDoExit")
if err != nil || c != "Broadcast hi 'there' DoExit" {
t.Fatalf("got %q, %v", c, err)
}
if _, err := Broadcast(" \n "); err == nil {
t.Fatal("empty message accepted")
}
if c, _ := SetTimeOfDay("6:30"); c != "SetTimeOfDay 6:30:00" {
t.Fatalf("got %q", c)
}
if _, err := SetTimeOfDay("25:00"); err == nil {
t.Fatal("bad time accepted")
}
if c, _ := ServerChatTo("76561198000000001", "hello"); c != `ServerChatTo "76561198000000001" hello` {
t.Fatalf("got %q", c)
}
}
func TestDuration(t *testing.T) {
for d, want := range map[time.Duration]string{
10 * time.Second: "10 seconds", time.Minute: "1 minute", 5 * time.Minute: "5 minutes", 3 * time.Hour: "3 hours",
} {
if got := Duration(d); got != want {
t.Errorf("%v: got %q, want %q", d, got, want)
}
}
}
// A fake game server that asks for a challenge first, as current servers do.
func TestQuery(t *testing.T) {
pc, err := net.ListenPacket("udp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer pc.Close()
go func() {
buf := make([]byte, 100)
for {
n, from, err := pc.ReadFrom(buf)
if err != nil {
return
}
if n == len(a2sRequest) {
pc.WriteTo([]byte{0xff, 0xff, 0xff, 0xff, 0x41, 1, 2, 3, 4}, from)
continue
}
reply := []byte{0xff, 0xff, 0xff, 0xff, 0x49, 17}
for _, s := range []string{"Test Island - (v350.2)", "TheIsland", "ark_survival_evolved", "ARK: Survival Evolved"} {
reply = append(append(reply, s...), 0)
}
reply = append(reply, 0, 0, 3, 20, 0, 'd', 'w', 0, 1, '1', '.', '0', 0)
pc.WriteTo(reply, from)
}
}()
in, err := Query(pc.LocalAddr().String(), 2*time.Second)
if err != nil {
t.Fatal(err)
}
if in.Name != "Test Island - (v350.2)" || in.Map != "TheIsland" || in.Players != 3 || in.MaxPlayers != 20 || in.Version != "350.2" {
t.Fatalf("got %+v", in)
}
}