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>
This commit is contained in:
commit
0d2cb31abf
32 files changed
+6280
No files matched your search
@@ -0,0 +1,91 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPasswordRoundTrip(t *testing.T) {
|
||||
h, err := HashPassword("correct horse")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !CheckPassword(h, "correct horse") || CheckPassword(h, "wrong") || CheckPassword("garbage", "x") {
|
||||
t.Fatal("password check is wrong")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoles(t *testing.T) {
|
||||
if !Admin.Allows(Moderator) || !Moderator.Allows(Viewer) || Viewer.Allows(Moderator) || Role("").Allows(Viewer) || Role("root").Allows(Viewer) {
|
||||
t.Fatal("role ordering is wrong")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
good := Config{Servers: []Server{{ID: "island", Host: "10.0.0.2", RCONPort: 27020}}}
|
||||
good.Defaults()
|
||||
if err := good.Validate(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cases := map[string]func(c *Config){
|
||||
"bad id": func(c *Config) { c.Servers[0].ID = "The Island" },
|
||||
"duplicate id": func(c *Config) { c.Servers = append(c.Servers, c.Servers[0]) },
|
||||
"no host": func(c *Config) { c.Servers[0].Host = "" },
|
||||
"bad port": func(c *Config) { c.Servers[0].RCONPort = 70000 },
|
||||
"bad user role": func(c *Config) { c.Users = []User{{Name: "a", Role: "root"}} },
|
||||
"job no timing": func(c *Config) { c.Jobs = []Job{{ID: "j", Name: "j", Action: "save"}} },
|
||||
"job both": func(c *Config) {
|
||||
c.Jobs = []Job{{ID: "j", Name: "j", Action: "save", Every: "1h", At: []string{"04:00"}}}
|
||||
},
|
||||
"job too often": func(c *Config) { c.Jobs = []Job{{ID: "j", Name: "j", Action: "save", Every: "5s"}} },
|
||||
"job bad time": func(c *Config) { c.Jobs = []Job{{ID: "j", Name: "j", Action: "save", At: []string{"4am"}}} },
|
||||
"job bad server": func(c *Config) {
|
||||
c.Jobs = []Job{{ID: "j", Name: "j", Action: "save", Every: "1h", Servers: []string{"nope"}}}
|
||||
},
|
||||
"job no message": func(c *Config) { c.Jobs = []Job{{ID: "j", Name: "j", Action: "broadcast", Every: "1h"}} },
|
||||
"notify bad url": func(c *Config) { c.Notify = []Notify{{Name: "n", Kind: "discord", URL: "javascript:alert(1)"}} },
|
||||
"filter action": func(c *Config) { c.ChatFilter.Action = "nuke" },
|
||||
}
|
||||
for name, mutate := range cases {
|
||||
c := clone(good)
|
||||
mutate(&c)
|
||||
if err := c.Validate(); err == nil {
|
||||
t.Errorf("%s: expected an error", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdatePersistsAndRejects(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "megalodon.json")
|
||||
h, err := Load(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if h.Exists() {
|
||||
t.Fatal("file should not exist before the first update")
|
||||
}
|
||||
err = h.Update(func(c *Config) error {
|
||||
c.Servers = append(c.Servers, Server{ID: "island", Host: "ark.lan", Password: "secret"})
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := h.Update(func(c *Config) error { c.Servers[0].ID = "BAD ID"; return nil }); err == nil || !strings.Contains(err.Error(), "server id") {
|
||||
t.Fatalf("invalid update accepted: %v", err)
|
||||
}
|
||||
h2, err := Load(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c := h2.Get()
|
||||
if len(c.Servers) != 1 || c.Servers[0].ID != "island" || c.Servers[0].Password != "secret" || c.Servers[0].RCONPort != 27020 {
|
||||
t.Fatalf("reloaded config is wrong: %+v", c.Servers)
|
||||
}
|
||||
// Get returns a copy.
|
||||
c.Servers[0].Host = "changed"
|
||||
if h2.Get().Servers[0].Host != "ark.lan" {
|
||||
t.Fatal("Get leaked internal state")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user