From b4dc5d6841f7ded5995e5f308509b1a3a034cbed Mon Sep 17 00:00:00 2001 From: Julien Dessaux Date: Thu, 24 Dec 2020 15:18:24 +0100 Subject: Began implementing config validation --- config/action.go | 54 ++++++++++++++++ config/action_test.go | 105 ++++++++++++++++++++++++++++++++ config/app.go | 37 +++++++++++ config/app_test.go | 59 ++++++++++++++++++ config/command.go | 32 ++++++++++ config/command_test.go | 51 ++++++++++++++++ config/config.go | 40 +++++++++++- config/config_test.go | 81 ++++++++++++++++++++---- config/game.go | 24 ++++++++ config/game_test.go | 10 +++ config/menu.go | 55 +++++++++++++++++ config/menu_test.go | 103 +++++++++++++++++++++++++++++++ config/test_data/invalid_app.yaml | 3 + config/test_data/invalid_menus.yaml | 58 ++++++++++++++++++ config/test_data/no_anonymous_menu.yaml | 47 ++++++++++++++ config/test_data/no_logged_in_menu.yaml | 46 ++++++++++++++ config/test_data/not_enough_menus.yaml | 29 +++++++++ example/complete.yaml | 32 +++++++--- go.mod | 6 +- go.sum | 4 ++ 20 files changed, 851 insertions(+), 25 deletions(-) create mode 100644 config/action.go create mode 100644 config/action_test.go create mode 100644 config/app_test.go create mode 100644 config/command.go create mode 100644 config/command_test.go create mode 100644 config/game_test.go create mode 100644 config/menu_test.go create mode 100644 config/test_data/invalid_app.yaml create mode 100644 config/test_data/invalid_menus.yaml create mode 100644 config/test_data/no_anonymous_menu.yaml create mode 100644 config/test_data/no_logged_in_menu.yaml create mode 100644 config/test_data/not_enough_menus.yaml 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, @@ -49,11 +88,6 @@ func TestLoadFile(t *testing.T) { Label: "watch", Action: "watch_menu", }, - MenuEntry{ - Key: "s", - Label: "scores", - Action: "scores", - }, MenuEntry{ Key: "q", Label: "quit", @@ -61,7 +95,7 @@ func TestLoadFile(t *testing.T) { }, }, }, - 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= -- cgit v1.2.3