aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJulien Dessaux2020-12-24 15:18:24 +0100
committerJulien Dessaux2020-12-24 15:18:24 +0100
commitb4dc5d6841f7ded5995e5f308509b1a3a034cbed (patch)
tree254466925238c53bd51372a57558ec68fdf78205
parentImplemented the configuration file format (diff)
downloadshell-game-launcher-b4dc5d6841f7ded5995e5f308509b1a3a034cbed.tar.gz
shell-game-launcher-b4dc5d6841f7ded5995e5f308509b1a3a034cbed.tar.bz2
shell-game-launcher-b4dc5d6841f7ded5995e5f308509b1a3a034cbed.zip
Began implementing config validation
-rw-r--r--config/action.go54
-rw-r--r--config/action_test.go105
-rw-r--r--config/app.go37
-rw-r--r--config/app_test.go59
-rw-r--r--config/command.go32
-rw-r--r--config/command_test.go51
-rw-r--r--config/config.go40
-rw-r--r--config/config_test.go81
-rw-r--r--config/game.go24
-rw-r--r--config/game_test.go10
-rw-r--r--config/menu.go55
-rw-r--r--config/menu_test.go103
-rw-r--r--config/test_data/invalid_app.yaml3
-rw-r--r--config/test_data/invalid_menus.yaml58
-rw-r--r--config/test_data/no_anonymous_menu.yaml47
-rw-r--r--config/test_data/no_logged_in_menu.yaml46
-rw-r--r--config/test_data/not_enough_menus.yaml29
-rw-r--r--example/complete.yaml32
-rw-r--r--go.mod6
-rw-r--r--go.sum4
20 files changed, 851 insertions, 25 deletions
diff --git a/config/action.go b/config/action.go
new file mode 100644
index 0000000..c63308e
--- /dev/null
+++ b/config/action.go
@@ -0,0 +1,54 @@
+package config
+
+import (
+ "strings"
+
+ "github.com/pkg/errors"
+)
+
+func validateAction(action string) error {
+ tokens := strings.Split(action, " ")
+ switch tokens[0] {
+ case "chmail":
+ if len(tokens) != 1 {
+ return errors.New("chmail action takes no arguments")
+ }
+ case "login":
+ if len(tokens) != 1 {
+ return errors.New("login action takes no arguments")
+ }
+ case "menu":
+ if len(tokens) != 2 {
+ return errors.New("menu action takes exactly one argument")
+ }
+ // menu existence is tested in global config
+ case "passwd":
+ if len(tokens) != 1 {
+ return errors.New("passwd action takes no arguments")
+ }
+ case "play":
+ if len(tokens) != 2 {
+ return errors.New("play action takes exactly one argument")
+ }
+ // game existence is tested in global config
+ case "register":
+ if len(tokens) != 1 {
+ return errors.New("register action takes no arguments")
+ }
+ case "replay":
+ if len(tokens) != 1 {
+ return errors.New("replay action takes no arguments")
+ }
+ case "watch":
+ if len(tokens) != 1 {
+ return errors.New("watch action takes no arguments")
+ }
+ case "quit":
+ if len(tokens) != 1 {
+ return errors.New("quit action takes no arguments")
+ }
+ default:
+ return errors.New("Invalid action : " + tokens[0])
+ }
+ return nil
+}
diff --git a/config/action_test.go b/config/action_test.go
new file mode 100644
index 0000000..dbc6ae3
--- /dev/null
+++ b/config/action_test.go
@@ -0,0 +1,105 @@
+package config
+
+import "testing"
+
+func TestActionValidate(t *testing.T) {
+ // Empty action
+ menuEntry := MenuEntry{Key: "l", Label: "label", Action: ""}
+ if err := menuEntry.validate(); err == nil {
+ t.Fatal("An action cannot be empty")
+ }
+ // Invalid action
+ menuEntry = MenuEntry{Key: "l", Label: "label", Action: "invalid"}
+ if err := menuEntry.validate(); err == nil {
+ t.Fatal("An action must be valid")
+ }
+ // chmail
+ menuEntry = MenuEntry{Key: "l", Label: "label", Action: "chmail a"}
+ if err := menuEntry.validate(); err == nil {
+ t.Fatal("chmail action does not take arguments")
+ }
+ menuEntry = MenuEntry{Key: "l", Label: "label", Action: "chmail"}
+ if err := menuEntry.validate(); err != nil {
+ t.Fatalf("chmail action without arguments is valid\nerror: +%v", err)
+ }
+ // login
+ menuEntry = MenuEntry{Key: "l", Label: "label", Action: "login a"}
+ if err := menuEntry.validate(); err == nil {
+ t.Fatal("login action does not take arguments")
+ }
+ menuEntry = MenuEntry{Key: "l", Label: "label", Action: "login"}
+ if err := menuEntry.validate(); err != nil {
+ t.Fatalf("login action without arguments is valid\nerror: +%v", err)
+ }
+ // menu
+ menuEntry = MenuEntry{Key: "l", Label: "label", Action: "menu"}
+ if err := menuEntry.validate(); err == nil {
+ t.Fatal("menu action takes exactly one argument")
+ }
+ menuEntry = MenuEntry{Key: "l", Label: "label", Action: "menu test plop"}
+ if err := menuEntry.validate(); err == nil {
+ t.Fatal("menu action takes exactly one argument")
+ }
+ menuEntry = MenuEntry{Key: "l", Label: "label", Action: "menu test"}
+ if err := menuEntry.validate(); err != nil {
+ t.Fatalf("menu action with one argument is valid\nerror: +%v", err)
+ }
+ // passwd
+ menuEntry = MenuEntry{Key: "l", Label: "label", Action: "passwd a"}
+ if err := menuEntry.validate(); err == nil {
+ t.Fatal("passwd action does not take arguments")
+ }
+ menuEntry = MenuEntry{Key: "l", Label: "label", Action: "passwd"}
+ if err := menuEntry.validate(); err != nil {
+ t.Fatalf("passwd action without arguments is valid\nerror: +%v", err)
+ }
+ // play
+ menuEntry = MenuEntry{Key: "l", Label: "label", Action: "play"}
+ if err := menuEntry.validate(); err == nil {
+ t.Fatal("play action takes exactly one argument")
+ }
+ menuEntry = MenuEntry{Key: "l", Label: "label", Action: "play test plop"}
+ if err := menuEntry.validate(); err == nil {
+ t.Fatal("play action takes exactly one argument")
+ }
+ menuEntry = MenuEntry{Key: "l", Label: "label", Action: "play test"}
+ if err := menuEntry.validate(); err != nil {
+ t.Fatalf("play action with one argument is valid\nerror: +%v", err)
+ }
+ // register
+ menuEntry = MenuEntry{Key: "l", Label: "label", Action: "register a"}
+ if err := menuEntry.validate(); err == nil {
+ t.Fatal("register action does not take arguments")
+ }
+ menuEntry = MenuEntry{Key: "l", Label: "label", Action: "register"}
+ if err := menuEntry.validate(); err != nil {
+ t.Fatalf("register action without arguments is valid\nerror: +%v", err)
+ }
+ // replay
+ menuEntry = MenuEntry{Key: "l", Label: "label", Action: "replay a"}
+ if err := menuEntry.validate(); err == nil {
+ t.Fatal("replay action does not take arguments")
+ }
+ menuEntry = MenuEntry{Key: "l", Label: "label", Action: "replay"}
+ if err := menuEntry.validate(); err != nil {
+ t.Fatalf("replay action without arguments is valid\nerror: +%v", err)
+ }
+ // watch
+ menuEntry = MenuEntry{Key: "l", Label: "label", Action: "watch a"}
+ if err := menuEntry.validate(); err == nil {
+ t.Fatal("watch action does not take arguments")
+ }
+ menuEntry = MenuEntry{Key: "l", Label: "label", Action: "watch"}
+ if err := menuEntry.validate(); err != nil {
+ t.Fatalf("watch action without arguments is valid\nerror: +%v", err)
+ }
+ // quit
+ menuEntry = MenuEntry{Key: "l", Label: "label", Action: "quit a"}
+ if err := menuEntry.validate(); err == nil {
+ t.Fatal("quit action does not take arguments")
+ }
+ menuEntry = MenuEntry{Key: "l", Label: "label", Action: "quit"}
+ if err := menuEntry.validate(); err != nil {
+ t.Fatalf("quit action without arguments is valid\nerror: +%v", err)
+ }
+}
diff --git a/config/app.go b/config/app.go
index 541699c..d7d1051 100644
--- a/config/app.go
+++ b/config/app.go
@@ -1,5 +1,12 @@
package config
+import (
+ "os"
+
+ "github.com/pkg/errors"
+ "golang.org/x/sys/unix"
+)
+
// App struct contains the configuration for this application
type App struct {
// WorkingDirectory is the program working directory where the user data, save files and scores are stored
@@ -15,3 +22,33 @@ type App struct {
// PostLoginCommands is the list of commands to execute upon login, like creating save directories for games
PostLoginCommands []string `yaml:"PostLoginCommands"`
}
+
+func (a *App) validate() error {
+ // WorkingDirectory
+ if err := os.MkdirAll(a.WorkingDirectory, 0700); err != nil {
+ return errors.Wrapf(err, "Invalid WorkingDirectory : %s", a.WorkingDirectory)
+ }
+ if err := unix.Access(a.WorkingDirectory, unix.W_OK); err != nil {
+ return errors.Wrapf(err, "Invalid WorkingDirectory : %s", a.WorkingDirectory)
+ }
+ // MaxUsers
+ if a.MaxUsers <= 0 {
+ return errors.New("MaxUsers must be a positive integer")
+ }
+ // AllowRegistration is just a bool, nothing to validate
+ // MaxNickLen
+ if a.MaxNickLen <= 0 {
+ return errors.New("MaxNickLen must be a positive integer")
+ }
+ // MenuMaxIdleTime
+ if a.MenuMaxIdleTime <= 0 {
+ return errors.New("MenuMaxIdleTime must be a positive integer")
+ }
+ // PostLoginCommands
+ for i := 0; i < len(a.PostLoginCommands); i++ {
+ if err := validateCommand(a.PostLoginCommands[i]); err != nil {
+ return errors.Wrap(err, "Failed to validate PostLoginCommands")
+ }
+ }
+ return nil
+}
diff --git a/config/app_test.go b/config/app_test.go
new file mode 100644
index 0000000..22fb891
--- /dev/null
+++ b/config/app_test.go
@@ -0,0 +1,59 @@
+package config
+
+import (
+ "os"
+ "testing"
+)
+
+func TestAppvalidate(t *testing.T) {
+ // WorkingDirectory
+ t.Cleanup(func() { os.RemoveAll("no_permission/") })
+ if err := os.Mkdir("no_permission/", 0000); err != nil {
+ t.Fatal("Could not create test directory")
+ }
+ app := App{WorkingDirectory: "no_permission/cannot_work"}
+ if err := app.validate(); err == nil {
+ t.Fatal("no_permission/cannot_wor/k should not be a valid working directory")
+ }
+ app = App{WorkingDirectory: "no_permission/"}
+ if err := app.validate(); err == nil {
+ t.Fatal("no_permission/ should not be a valid working directory")
+ }
+
+ // MaxUsers
+ t.Cleanup(func() { os.RemoveAll("var/") })
+ app = App{
+ WorkingDirectory: "var/",
+ MaxUsers: 0,
+ }
+ if err := app.validate(); err == nil {
+ t.Fatal("Negative MaxUsers should not be valid")
+ }
+
+ // AllowRegistration is just a bool, nothing to test
+
+ // MaxNickLen
+ t.Cleanup(func() { os.RemoveAll("var/") })
+ app = App{
+ WorkingDirectory: "var/",
+ MaxUsers: 1,
+ MaxNickLen: 0,
+ }
+ if err := app.validate(); err == nil {
+ t.Fatal("Negative or zero MaxNickLen should not be valid.")
+ }
+
+ //MenuMaxIdleTime
+ t.Cleanup(func() { os.RemoveAll("var/") })
+ app = App{
+ WorkingDirectory: "var/",
+ MaxUsers: 512,
+ MaxNickLen: 15,
+ MenuMaxIdleTime: 0,
+ }
+ if err := app.validate(); err == nil {
+ t.Fatal("Negative or zero MenuMaxIdleTime should not be valid.")
+ }
+
+ //PostLoginCommands is tested from command.go
+}
diff --git a/config/command.go b/config/command.go
new file mode 100644
index 0000000..34142cd
--- /dev/null
+++ b/config/command.go
@@ -0,0 +1,32 @@
+package config
+
+import (
+ "strings"
+
+ "github.com/pkg/errors"
+)
+
+func validateCommand(cmd string) error {
+ tokens := strings.Split(cmd, " ")
+ switch tokens[0] {
+ case "cp":
+ if len(tokens) != 3 {
+ return errors.New("cp command takes exactly two arguments")
+ }
+ case "exec":
+ if len(tokens) <= 1 {
+ return errors.New("exec command needs arguments")
+ }
+ case "mkdir":
+ if len(tokens) != 2 {
+ return errors.New("mkdir command takes exactly one argument")
+ }
+ case "wait":
+ if len(tokens) != 1 {
+ return errors.New("wait command takes no arguments")
+ }
+ default:
+ return errors.New("Invalid command : " + tokens[0])
+ }
+ return nil
+}
diff --git a/config/command_test.go b/config/command_test.go
new file mode 100644
index 0000000..7e07956
--- /dev/null
+++ b/config/command_test.go
@@ -0,0 +1,51 @@
+package config
+
+import "testing"
+
+func TestCommandValidate(t *testing.T) {
+ // Empty command
+ if err := validateCommand(""); err == nil {
+ t.Fatal("An command cannot be empty")
+ }
+ // invalid command
+ if err := validateCommand("invalid"); err == nil {
+ t.Fatal("An command cannot be empty")
+ }
+ // cp
+ if err := validateCommand("cp"); err == nil {
+ t.Fatal("cp command needs arguments")
+ }
+ if err := validateCommand("cp test"); err == nil {
+ t.Fatal("cp command needs exactly 2 arguments")
+ }
+ if err := validateCommand("cp test test test"); err == nil {
+ t.Fatal("cp command needs exactly 2 arguments")
+ }
+ if err := validateCommand("exec test test"); err != nil {
+ t.Fatal("valid exec command should be accepted")
+ }
+ // exec
+ if err := validateCommand("exec"); err == nil {
+ t.Fatal("exec command needs arguments")
+ }
+ if err := validateCommand("exec test"); err != nil {
+ t.Fatal("valid exec command should be accepted")
+ }
+ // mkdir
+ if err := validateCommand("mkdir"); err == nil {
+ t.Fatal("mkdir command needs exactly 1 argument")
+ }
+ if err := validateCommand("mkdir testtest "); err == nil {
+ t.Fatal("mkdir command needs exactly 1 argument")
+ }
+ if err := validateCommand("mkdir test"); err != nil {
+ t.Fatal("valid mkdir command should be accepted")
+ }
+ // wait
+ if err := validateCommand("wait test"); err == nil {
+ t.Fatal("wait command needs no arguments")
+ }
+ if err := validateCommand("wait"); err != nil {
+ t.Fatal("valid wait command should be accepted")
+ }
+}
diff --git a/config/config.go b/config/config.go
index 2c94dbe..fada1ea 100644
--- a/config/config.go
+++ b/config/config.go
@@ -3,6 +3,7 @@ package config
import (
"os"
+ "github.com/pkg/errors"
"gopkg.in/yaml.v2"
)
@@ -10,11 +11,47 @@ type Config struct {
// AppConfig is the application level configuration entries
App App `yaml:"App"`
// Menus is the list of menus. The first one is the default menu for an anonymous user, the second one is the default menu for an authenticated user
- Menus []Menu `yaml:"Menus"`
+ Menus map[string]Menu `yaml:"Menus"`
// Games is the list of games.
Games map[string]Game `yaml:"Games"`
}
+func (c *Config) validate() error {
+ if err := c.App.validate(); err != nil {
+ return err
+ }
+ if len(c.Menus) < 2 {
+ return errors.New("A valid configuration needs at least two menu entries named anonymous and logged_in")
+ }
+ found_anonymous_menu := false
+ found_logged_in_menu := false
+ for k, v := range c.Menus {
+ if err := v.validate(k); err != nil {
+ return err
+ }
+ if k == "anonymous" {
+ found_anonymous_menu = true
+ }
+ if k == "logged_in" {
+ found_logged_in_menu = true
+ }
+ }
+ if !found_anonymous_menu {
+ return errors.New("No anonymous menu declared")
+ }
+ if !found_logged_in_menu {
+ return errors.New("No logged_in menu declared")
+ }
+ for k, v := range c.Games {
+ if err := v.validate(k); err != nil {
+ return err
+ }
+ }
+ // TODO menu existence is tested in global config
+ // TODO game existence is tested in global config
+ return nil
+}
+
// LoadFile loads the config from a given file
func LoadFile(path string) (config Config, err error) {
var f *os.File
@@ -27,5 +64,6 @@ func LoadFile(path string) (config Config, err error) {
if err = decoder.Decode(&config); err != nil {
return
}
+ err = config.validate()
return
}
diff --git a/config/config_test.go b/config/config_test.go
index adbfc7e..1fcc57c 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -1,19 +1,58 @@
package config
import (
+ "os"
"reflect"
"testing"
)
func TestLoadFile(t *testing.T) {
+ // Non existant file
_, err := LoadFile("test_data/non-existant")
if err == nil {
t.Fatal("non-existant config file failed without error")
}
+
+ // Invalid yaml file
_, err = LoadFile("test_data/invalid_yaml")
if err == nil {
t.Fatal("invalid_yaml config file failed without error")
}
+
+ // TODO test non existant menu in action menu entries, and duplicate, and that anonymous and logged_in exist
+ // TODO test non existant game in play actions, and duplicate
+ //menuEntry = MenuEntry{
+ //Key: "p",
+ //Label: "play non existant game",
+ //Action: "play nonexistant",
+ //}
+ //if err := menuEntry.validate(); err == nil {
+ //t.Fatal("An inexistant game cannot be played")
+ //}
+
+ t.Cleanup(func() { os.RemoveAll("var/") })
+ // Invalid App example
+ if _, err := LoadFile("test_data/invalid_app.yaml"); err == nil {
+ t.Fatal("Invalid App entry should fail to load")
+ }
+ // Not enough menus example
+ if _, err := LoadFile("test_data/not_enough_menus.yaml"); err == nil {
+ t.Fatal("not enough menu entries should fail to load")
+ }
+ // Invalid Menus example
+ if _, err := LoadFile("test_data/invalid_menus.yaml"); err == nil {
+ t.Fatal("Invalid menu entry should fail to load")
+ }
+ // no anonymous Menu example
+ if _, err := LoadFile("test_data/no_anonymous_menu.yaml"); err == nil {
+ t.Fatal("Invalid menu entry should fail to load")
+ }
+ // no logged_in Menu example
+ if _, err := LoadFile("test_data/no_logged_in_menu.yaml"); err == nil {
+ t.Fatal("Invalid menu entry should fail to load")
+ }
+
+ // Complexe example
config, err := LoadFile("../example/complete.yaml")
want := Config{
App: App{
@@ -28,8 +67,8 @@ func TestLoadFile(t *testing.T) {
"mkdir %w/userdata/%u/ttyrec",
},
},
- Menus: []Menu{
- Menu{
+ Menus: map[string]Menu{
+ "anonymous": Menu{
Banner: "Shell Game Launcher - Anonymous access%n======================================",
XOffset: 5,
YOffset: 2,
@@ -50,18 +89,13 @@ func TestLoadFile(t *testing.T) {
Action: "watch_menu",
},
MenuEntry{
- Key: "s",
- Label: "scores",
- Action: "scores",
- },
- MenuEntry{
Key: "q",
Label: "quit",
Action: "quit",
},
},
},
- Menu{
+ "logged_in": Menu{
Banner: "Shell Game Launcher%n===================",
XOffset: 5,
YOffset: 2,
@@ -74,17 +108,27 @@ func TestLoadFile(t *testing.T) {
MenuEntry{
Key: "o",
Label: "edit game options",
- Action: "options",
+ Action: "menu options",
},
MenuEntry{
Key: "w",
Label: "watch",
- Action: "watch_menu",
+ Action: "watch",
+ },
+ MenuEntry{
+ Key: "r",
+ Label: "replay",
+ Action: "replay",
},
MenuEntry{
- Key: "s",
- Label: "scores",
- Action: "scores",
+ Key: "c",
+ Label: "change password",
+ Action: "passwd",
+ },
+ MenuEntry{
+ Key: "m",
+ Label: "change email",
+ Action: "chmail",
},
MenuEntry{
Key: "q",
@@ -98,6 +142,17 @@ func TestLoadFile(t *testing.T) {
"nethack3.7": Game{
ChrootPath: "/opt/nethack",
FileMode: "0666",
+ ScoreCommands: []string{
+ "exec /games/nethack -s all",
+ "wait",
+ },
+ Commands: []string{
+ "cp /games/var/save/%u%n.gz /games/var/save/%u%n.gz.bak",
+ "exec /games/nethack -u %n",
+ },
+ Env: map[string]string{
+ "NETHACKOPTIONS": "@%ruserdata/%n/%n.nhrc",
+ },
},
},
}
diff --git a/config/game.go b/config/game.go
index 0eea917..5520659 100644
--- a/config/game.go
+++ b/config/game.go
@@ -1,5 +1,12 @@
package config
+import (
+ "errors"
+ "regexp"
+)
+
+var reValidGameName = regexp.MustCompile(`^[\w\._]+$`)
+
// Game struct containers the configuration for a game
type Game struct {
// ChrootPath is the chroot path for the game
@@ -8,4 +15,21 @@ type Game struct {
FileMode string `yaml:"FileMode"`
// Commands is the command list
Commands []string `yaml:"Commands"`
+ // ScoreFile is relative to the chroot path for the game
+ ScoreCommands []string `yaml:"ScoreCommands"`
+ // Env is the environment in which to exec the commands
+ Env map[string]string `yaml:"Env"`
+}
+
+func (a *Game) validate(name string) error {
+ // Game name
+ if ok := reValidGameName.MatchString(name); !ok {
+ return errors.New("Invalid Game name, must match regex `^[\\w\\._]+$` : " + name)
+ }
+ // ChrootPath TODO
+ // FileMode
+ // Commands
+ // ScoreFile
+ // Env
+ return nil
}
diff --git a/config/game_test.go b/config/game_test.go
new file mode 100644
index 0000000..d295ce2
--- /dev/null
+++ b/config/game_test.go
@@ -0,0 +1,10 @@
+package config
+
+import "testing"
+
+func TestGameValidate(t *testing.T) {
+ empty := Game{}
+ if err := empty.validate("invalid game name because of spaces"); err == nil {
+ t.Fatal("invalid game name")
+ }
+}
diff --git a/config/menu.go b/config/menu.go
index 2913be2..160afa3 100644
--- a/config/menu.go
+++ b/config/menu.go
@@ -1,5 +1,14 @@
package config
+import (
+ "regexp"
+
+ "github.com/pkg/errors"
+)
+
+var reValidMenuName = regexp.MustCompile(`^[\w\._]+$`)
+var reValidKey = regexp.MustCompile(`^\w$`)
+
// Menu struct describes a screen menu
type Menu struct {
// Banner is the banner to display before the menu
@@ -21,3 +30,49 @@ type MenuEntry struct {
// Action is the action executed when the menu entry is selected
Action string `yaml:"Action"`
}
+
+func (m *Menu) validate(name string) error {
+ // validate name
+ if ok := reValidMenuName.MatchString(name); !ok {
+ return errors.New("Invalid menu name, must be an alphanumeric word and match regex `^[\\w\\._]+$` : " + name)
+ }
+ // Banner is just any string, nothing to validate
+ // XOffset
+ if m.XOffset <= 0 {
+ return errors.New("XOffset must be a positive integer")
+ }
+ // YOffset
+ if m.YOffset <= 0 {
+ return errors.New("YOffset must be a positive integer")
+ }
+ // MenuEntries
+ keys := make(map[string]bool)
+ for i := 0; i < len(m.MenuEntries); i++ {
+ m.MenuEntries[i].validate()
+ if _, duplicate := keys[m.MenuEntries[i].Key]; duplicate {
+ return errors.New("A Menu has a duplicate key " + m.MenuEntries[i].Key)
+ }
+ keys[m.MenuEntries[i].Key] = true
+ if m.MenuEntries[i].Action == "menu "+name {
+ return errors.New("A menu shall not loop on itself")
+ }
+ }
+ // Loop test
+ return nil
+}
+
+func (m *MenuEntry) validate() error {
+ // Key
+ if ok := reValidKey.MatchString(m.Key); !ok {
+ return errors.New("Invalid Key, must be exactly one alphanumeric character and match regex `^\\w$` : " + m.Key)
+ }
+ // Label
+ if len(m.Label) <= 0 {
+ return errors.New("Invalid Label, cannot be empty")
+ }
+ // Action
+ if err := validateAction(m.Action); err != nil {
+ return errors.Wrap(err, "Invalid Action in MenuEntry")
+ }
+ return nil
+}
diff --git a/config/menu_test.go b/config/menu_test.go
new file mode 100644
index 0000000..98405c7
--- /dev/null
+++ b/config/menu_test.go
@@ -0,0 +1,103 @@
+package config
+
+import "testing"
+
+func TestMenuValidate(t *testing.T) {
+ // menu name
+ menu := Menu{}
+ if err := menu.validate(""); err == nil {
+ t.Fatal("Empty menu name is not valid")
+ }
+ if err := menu.validate("test test"); err == nil {
+ t.Fatal("non alphanumeric menu name is not valid")
+ }
+ // Banner is just any string, nothing to validate
+ // XOffset
+ menu = Menu{XOffset: -1}
+ if err := menu.validate("test"); err == nil {
+ t.Fatal("Negative XOffset should not be valid")
+ }
+ // YOffset
+ menu = Menu{
+ XOffset: 1,
+ YOffset: -1,
+ }
+ if err := menu.validate("test"); err == nil {
+ t.Fatal("Negative YOffset should not be valid")
+ }
+ // MenuEntries are mostly tested bellow
+ menu = Menu{
+ XOffset: 1,
+ YOffset: 1,
+ MenuEntries: []MenuEntry{
+ MenuEntry{
+ Key: "a",
+ Label: "test",
+ Action: "quit",
+ },
+ MenuEntry{
+ Key: "a",
+ Label: "duplicate",
+ Action: "quit",
+ },
+ },
+ }
+ if err := menu.validate("test"); err == nil {
+ t.Fatal("Duplicate Keys in MenuEntries should not be allowed")
+ }
+ // loop menu
+ menu = Menu{
+ XOffset: 1,
+ YOffset: 1,
+ MenuEntries: []MenuEntry{
+ MenuEntry{
+ Key: "a",
+ Label: "test",
+ Action: "menu test",
+ },
+ },
+ }
+ if err := menu.validate("test"); err == nil {
+ t.Fatal("A menu should not be able to loop on itself")
+ }
+ // A valid menu
+ menu = Menu{
+ XOffset: 1,
+ YOffset: 1,
+ MenuEntries: []MenuEntry{
+ MenuEntry{
+ Key: "a",
+ Label: "test",
+ Action: "quit",
+ },
+ },
+ }
+ if err := menu.validate("test"); err != nil {
+ t.Fatal("A valid menu should pass")
+ }
+}
+
+func TestMenuEntryValidate(t *testing.T) {
+ // Key
+ menuEntry := MenuEntry{}
+ if err := menuEntry.validate(); err == nil {
+ t.Fatal("A Key cannot be empty")
+ }
+ menuEntry = MenuEntry{Key: "ab"}
+ if err := menuEntry.validate(); err == nil {
+ t.Fatal("A Key should be only one character")
+ }
+ menuEntry = MenuEntry{Key: " "}
+ if err := menuEntry.validate(); err == nil {
+ t.Fatal("A Key should be a printable character")
+ }
+ // Label
+ menuEntry = MenuEntry{
+ Key: "l",
+ Label: "",
+ }
+ if err := menuEntry.validate(); err == nil {
+ t.Fatal("A Label cannot be empty")
+ }
+ // Actions are tested in action_test.go
+}
diff --git a/config/test_data/invalid_app.yaml b/config/test_data/invalid_app.yaml
new file mode 100644
index 0000000..1986c63
--- /dev/null
+++ b/config/test_data/invalid_app.yaml
@@ -0,0 +1,3 @@
+App:
+ WorkingDirectory: var/
+ MaxUsers: -1
diff --git a/config/test_data/invalid_menus.yaml b/config/test_data/invalid_menus.yaml
new file mode 100644
index 0000000..49a321e
--- /dev/null
+++ b/config/test_data/invalid_menus.yaml
@@ -0,0 +1,58 @@
+App:
+ WorkingDirectory: var/
+ MaxUsers: 512
+ AllowRegistration: true
+ MaxNickLen: 15
+ MenuMaxIdleTime: 600
+ PostLoginCommands:
+ - mkdir %w/userdata/%u
+ - mkdir %w/userdata/%u/dumplog
+ - mkdir %w/userdata/%u/ttyrec
+
+Menus:
+ anonymous:
+ Banner: 'Shell Game Launcher - Anonymous access%n======================================'
+ XOffset: 5
+ YOffset: 2
+ MenuEntries:
+ - Key: l
+ Label: login
+ Action: login
+ - Key: r
+ Label: register
+ Action: register
+ - Key: w
+ Label: watch
+ Action: watch_menu
+ - Key: q
+ Label: quit
+ Action: quit
+ logged_in:
+ Banner: 'Shell Game Launcher%n==================='
+ XOffset: 5
+ YOffset: 2
+ MenuEntries:
+ - Key: p
+ Label: play Nethack 3.7
+ Action: play nethack3.7
+ - Key: o
+ Label: edit game options
+ Action: menu options
+ - Key: w
+ Label: watch
+ Action: watch
+ - Key: r
+ Label: replay
+ Action: replay
+ - Key: c
+ Label: change password
+ Action: passwd
+ - Key: m
+ Label: change email
+ Action: chmail
+ - Key: q
+ Label: quit
+ Action: quit
+ test:
+ Banner: 'Shell Game Launcher%n==================='
+ XOffset: -1
diff --git a/config/test_data/no_anonymous_menu.yaml b/config/test_data/no_anonymous_menu.yaml
new file mode 100644
index 0000000..2afc4ad
--- /dev/null
+++ b/config/test_data/no_anonymous_menu.yaml
@@ -0,0 +1,47 @@
+App:
+ WorkingDirectory: var/
+ MaxUsers: 512
+ AllowRegistration: true
+ MaxNickLen: 15
+ MenuMaxIdleTime: 600
+ PostLoginCommands:
+ - mkdir %w/userdata/%u
+ - mkdir %w/userdata/%u/dumplog
+ - mkdir %w/userdata/%u/ttyrec
+
+Menus:
+ logged_in:
+ Banner: 'Shell Game Launcher - Anonymous access%n======================================'
+ XOffset: 5
+ YOffset: 2
+ MenuEntries:
+ - Key: l
+ Label: login
+ Action: login
+ - Key: r
+ Label: register
+ Action: register
+ - Key: w
+ Label: watch
+ Action: watch_menu
+ - Key: q
+ Label: quit
+ Action: quit
+ test:
+ Banner: 'Shell Game Launcher - Anonymous access%n======================================'
+ XOffset: 5
+ YOffset: 2
+ MenuEntries:
+ - Key: l
+ Label: login
+ Action: login
+ - Key: r
+ Label: register
+ Action: register
+ - Key: w
+ Label: watch
+ Action: watch_menu
+ - Key: q
+ Label: quit
+ Action: quit
+
diff --git a/config/test_data/no_logged_in_menu.yaml b/config/test_data/no_logged_in_menu.yaml
new file mode 100644
index 0000000..f6e39fa
--- /dev/null
+++ b/config/test_data/no_logged_in_menu.yaml
@@ -0,0 +1,46 @@
+App:
+ WorkingDirectory: var/
+ MaxUsers: 512
+ AllowRegistration: true
+ MaxNickLen: 15
+ MenuMaxIdleTime: 600
+ PostLoginCommands:
+ - mkdir %w/userdata/%u
+ - mkdir %w/userdata/%u/dumplog
+ - mkdir %w/userdata/%u/ttyrec
+
+Menus:
+ anonymous:
+ Banner: 'Shell Game Launcher - Anonymous access%n======================================'
+ XOffset: 5
+ YOffset: 2
+ MenuEntries:
+ - Key: l
+ Label: login
+ Action: login
+ - Key: r
+ Label: register
+ Action: register
+ - Key: w
+ Label: watch
+ Action: watch_menu
+ - Key: q
+ Label: quit
+ Action: quit
+ test:
+ Banner: 'Shell Game Launcher - Anonymous access%n======================================'
+ XOffset: 5
+ YOffset: 2
+ MenuEntries:
+ - Key: l
+ Label: login
+ Action: login
+ - Key: r
+ Label: register
+ Action: register
+ - Key: w
+ Label: watch
+ Action: watch_menu
+ - Key: q
+ Label: quit
+ Action: quit
diff --git a/config/test_data/not_enough_menus.yaml b/config/test_data/not_enough_menus.yaml
new file mode 100644
index 0000000..db3b72c
--- /dev/null
+++ b/config/test_data/not_enough_menus.yaml
@@ -0,0 +1,29 @@
+App:
+ WorkingDirectory: var/
+ MaxUsers: 512
+ AllowRegistration: true
+ MaxNickLen: 15
+ MenuMaxIdleTime: 600
+ PostLoginCommands:
+ - mkdir %w/userdata/%u
+ - mkdir %w/userdata/%u/dumplog
+ - mkdir %w/userdata/%u/ttyrec
+
+Menus:
+ anonymous:
+ Banner: 'Shell Game Launcher - Anonymous access%n======================================'
+ XOffset: 5
+ YOffset: 2
+ MenuEntries:
+ - Key: l
+ Label: login
+ Action: login
+ - Key: r
+ Label: register
+ Action: register
+ - Key: w
+ Label: watch
+ Action: watch_menu
+ - Key: q
+ Label: quit
+ Action: quit
diff --git a/example/complete.yaml b/example/complete.yaml
index 31cea74..e8e36e1 100644
--- a/example/complete.yaml
+++ b/example/complete.yaml
@@ -10,7 +10,8 @@ App:
- mkdir %w/userdata/%u/ttyrec
Menus:
- - Banner: 'Shell Game Launcher - Anonymous access%n======================================'
+ anonymous:
+ Banner: 'Shell Game Launcher - Anonymous access%n======================================'
XOffset: 5
YOffset: 2
MenuEntries:
@@ -23,13 +24,11 @@ Menus:
- Key: w
Label: watch
Action: watch_menu
- - Key: s
- Label: scores
- Action: scores
- Key: q
Label: quit
Action: quit
- - Banner: 'Shell Game Launcher%n==================='
+ logged_in:
+ Banner: 'Shell Game Launcher%n==================='
XOffset: 5
YOffset: 2
MenuEntries:
@@ -38,13 +37,19 @@ Menus:
Action: play nethack3.7
- Key: o
Label: edit game options
- Action: options
+ Action: menu options
- Key: w
Label: watch
- Action: watch_menu
- - Key: s
- Label: scores
- Action: scores
+ Action: watch
+ - Key: r
+ Label: replay
+ Action: replay
+ - Key: c
+ Label: change password
+ Action: passwd
+ - Key: m
+ Label: change email
+ Action: chmail
- Key: q
Label: quit
Action: quit
@@ -53,4 +58,11 @@ Games:
nethack3.7:
ChrootPath: /opt/nethack
FileMode: "0666"
+ ScoreCommands:
+ - exec /games/nethack -s all
+ - wait
Commands:
+ - cp /games/var/save/%u%n.gz /games/var/save/%u%n.gz.bak
+ - exec /games/nethack -u %n
+ Env:
+ NETHACKOPTIONS: "@%ruserdata/%n/%n.nhrc"
diff --git a/go.mod b/go.mod
index 4237bdf..f43c8d2 100644
--- a/go.mod
+++ b/go.mod
@@ -2,4 +2,8 @@ module shell-game-launcher
go 1.15
-require gopkg.in/yaml.v2 v2.4.0
+require (
+ github.com/pkg/errors v0.9.1
+ golang.org/x/sys v0.0.0-20201223074533-0d417f636930
+ gopkg.in/yaml.v2 v2.4.0
+)
diff --git a/go.sum b/go.sum
index dd0bc19..8b4f631 100644
--- a/go.sum
+++ b/go.sum
@@ -1,3 +1,7 @@
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+golang.org/x/sys v0.0.0-20201223074533-0d417f636930 h1:vRgIt+nup/B/BwIS0g2oC0haq0iqbV3ZA+u6+0TlNCo=
+golang.org/x/sys v0.0.0-20201223074533-0d417f636930/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=