From c3263c03776401ad1263a9fb8f5a44a8ed44d61b Mon Sep 17 00:00:00 2001 From: Julien Dessaux Date: Wed, 17 Nov 2021 10:13:06 +0100 Subject: Refactored package structure --- client/input.go | 26 --- client/input_test.go | 60 ------ client/menu.go | 17 -- client/menu_test.go | 92 --------- client/state.go | 23 --- client/state_test.go | 14 -- config/action.go | 54 ------ config/action_test.go | 105 ---------- config/app.go | 54 ------ config/app_test.go | 90 --------- config/command.go | 32 ---- config/command_test.go | 51 ----- config/config.go | 64 ------- config/config_test.go | 221 ---------------------- config/game.go | 63 ------ config/game_test.go | 107 ----------- config/menu.go | 113 ----------- config/menu_test.go | 71 ------- config/test_data/duplicate_game.yaml | 30 --- config/test_data/duplicate_menu.yaml | 28 --- config/test_data/fake_nethack_directory/.keep | 0 config/test_data/invalid_app.yaml | 11 -- config/test_data/invalid_game.yaml | 21 -- config/test_data/invalid_menus.yaml | 19 -- config/test_data/invalid_yaml | 1 - config/test_data/minimal.yaml | 18 -- config/test_data/no_anonymous_menu.yaml | 18 -- config/test_data/no_logged_in_menu.yaml | 18 -- config/test_data/non_existant_chopts.yaml | 18 -- config/test_data/non_existant_game.yaml | 18 -- config/test_data/non_existant_menu.yaml | 18 -- config/test_data/not_enough_menus.yaml | 13 -- config/test_data/unreachable_game.yaml | 25 --- config/test_data/unreachable_menu.yaml | 23 --- pkg/client/input.go | 26 +++ pkg/client/input_test.go | 60 ++++++ pkg/client/menu.go | 17 ++ pkg/client/menu_test.go | 92 +++++++++ pkg/client/state.go | 23 +++ pkg/client/state_test.go | 14 ++ pkg/config/action.go | 54 ++++++ pkg/config/action_test.go | 105 ++++++++++ pkg/config/app.go | 55 ++++++ pkg/config/app_test.go | 90 +++++++++ pkg/config/command.go | 32 ++++ pkg/config/command_test.go | 51 +++++ pkg/config/config.go | 64 +++++++ pkg/config/config_test.go | 221 ++++++++++++++++++++++ pkg/config/game.go | 63 ++++++ pkg/config/game_test.go | 107 +++++++++++ pkg/config/menu.go | 113 +++++++++++ pkg/config/menu_test.go | 71 +++++++ pkg/config/test_data/duplicate_game.yaml | 30 +++ pkg/config/test_data/duplicate_menu.yaml | 28 +++ pkg/config/test_data/fake_nethack_directory/.keep | 0 pkg/config/test_data/invalid_app.yaml | 11 ++ pkg/config/test_data/invalid_game.yaml | 21 ++ pkg/config/test_data/invalid_menus.yaml | 19 ++ pkg/config/test_data/invalid_yaml | 1 + pkg/config/test_data/minimal.yaml | 18 ++ pkg/config/test_data/no_anonymous_menu.yaml | 18 ++ pkg/config/test_data/no_logged_in_menu.yaml | 18 ++ pkg/config/test_data/non_existant_chopts.yaml | 18 ++ pkg/config/test_data/non_existant_game.yaml | 18 ++ pkg/config/test_data/non_existant_menu.yaml | 18 ++ pkg/config/test_data/not_enough_menus.yaml | 13 ++ pkg/config/test_data/unreachable_game.yaml | 25 +++ pkg/config/test_data/unreachable_menu.yaml | 23 +++ 68 files changed, 1537 insertions(+), 1536 deletions(-) delete mode 100644 client/input.go delete mode 100644 client/input_test.go delete mode 100644 client/menu.go delete mode 100644 client/menu_test.go delete mode 100644 client/state.go delete mode 100644 client/state_test.go delete mode 100644 config/action.go delete mode 100644 config/action_test.go delete mode 100644 config/app.go delete mode 100644 config/app_test.go delete mode 100644 config/command.go delete mode 100644 config/command_test.go delete mode 100644 config/config.go delete mode 100644 config/config_test.go delete mode 100644 config/game.go delete mode 100644 config/game_test.go delete mode 100644 config/menu.go delete mode 100644 config/menu_test.go delete mode 100644 config/test_data/duplicate_game.yaml delete mode 100644 config/test_data/duplicate_menu.yaml delete mode 100644 config/test_data/fake_nethack_directory/.keep delete mode 100644 config/test_data/invalid_app.yaml delete mode 100644 config/test_data/invalid_game.yaml delete mode 100644 config/test_data/invalid_menus.yaml delete mode 100644 config/test_data/invalid_yaml delete mode 100644 config/test_data/minimal.yaml delete mode 100644 config/test_data/no_anonymous_menu.yaml delete mode 100644 config/test_data/no_logged_in_menu.yaml delete mode 100644 config/test_data/non_existant_chopts.yaml delete mode 100644 config/test_data/non_existant_game.yaml delete mode 100644 config/test_data/non_existant_menu.yaml delete mode 100644 config/test_data/not_enough_menus.yaml delete mode 100644 config/test_data/unreachable_game.yaml delete mode 100644 config/test_data/unreachable_menu.yaml create mode 100644 pkg/client/input.go create mode 100644 pkg/client/input_test.go create mode 100644 pkg/client/menu.go create mode 100644 pkg/client/menu_test.go create mode 100644 pkg/client/state.go create mode 100644 pkg/client/state_test.go create mode 100644 pkg/config/action.go create mode 100644 pkg/config/action_test.go create mode 100644 pkg/config/app.go create mode 100644 pkg/config/app_test.go create mode 100644 pkg/config/command.go create mode 100644 pkg/config/command_test.go create mode 100644 pkg/config/config.go create mode 100644 pkg/config/config_test.go create mode 100644 pkg/config/game.go create mode 100644 pkg/config/game_test.go create mode 100644 pkg/config/menu.go create mode 100644 pkg/config/menu_test.go create mode 100644 pkg/config/test_data/duplicate_game.yaml create mode 100644 pkg/config/test_data/duplicate_menu.yaml create mode 100644 pkg/config/test_data/fake_nethack_directory/.keep create mode 100644 pkg/config/test_data/invalid_app.yaml create mode 100644 pkg/config/test_data/invalid_game.yaml create mode 100644 pkg/config/test_data/invalid_menus.yaml create mode 100644 pkg/config/test_data/invalid_yaml create mode 100644 pkg/config/test_data/minimal.yaml create mode 100644 pkg/config/test_data/no_anonymous_menu.yaml create mode 100644 pkg/config/test_data/no_logged_in_menu.yaml create mode 100644 pkg/config/test_data/non_existant_chopts.yaml create mode 100644 pkg/config/test_data/non_existant_game.yaml create mode 100644 pkg/config/test_data/non_existant_menu.yaml create mode 100644 pkg/config/test_data/not_enough_menus.yaml create mode 100644 pkg/config/test_data/unreachable_game.yaml create mode 100644 pkg/config/test_data/unreachable_menu.yaml diff --git a/client/input.go b/client/input.go deleted file mode 100644 index 8c814a6..0000000 --- a/client/input.go +++ /dev/null @@ -1,26 +0,0 @@ -package client - -import ( - "bufio" - "os" - - "github.com/pkg/errors" -) - -// getValidInput returns the selected menu command as a string or an error -func (s *State) getValidInput() (string, error) { - menu := s.config.Menus[s.currentMenu] - - reader := bufio.NewReader(os.Stdin) - for { - input, err := reader.ReadByte() - if err != nil { - return "", errors.Wrapf(err, "Could not read byte from stdin") - } - for _, menuEntry := range menu.MenuEntries { - if []byte(menuEntry.Key)[0] == input { - return menuEntry.Action, nil - } - } - } -} diff --git a/client/input_test.go b/client/input_test.go deleted file mode 100644 index fe8feeb..0000000 --- a/client/input_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package client - -import ( - "os" - "shell-game-launcher/config" - "testing" -) - -func TestGetValidInput(t *testing.T) { - realStdin := os.Stdin - t.Cleanup(func() { os.Stdin = realStdin }) - - // Complete menu, no input error - state := State{ - config: &config.Config{ - Menus: map[string]config.Menu{ - "test": config.Menu{ - Banner: "TEST TEST TEST", - MenuEntries: []config.MenuEntry{ - config.MenuEntry{ - Key: "w", - Label: "wait entry", - Action: "wait", - }, - config.MenuEntry{ - Key: "q", - Label: "quit entry", - Action: "quit", - }, - }, - }, - }, - }, - currentMenu: "test", - login: "", - } - r, w, _ := os.Pipe() - os.Stdin = r - - // Simply test quit entry - w.WriteString("q") - if cmd, err := state.getValidInput(); err != nil || cmd != "quit" { - t.Fatalf("Input handled incorrectly:\nwant: wait\ngot: %s\nerror: %s\n", cmd, err) - } - // test quit entry after wrong keys - w.WriteString("abcdq") - if cmd, err := state.getValidInput(); err != nil || cmd != "quit" { - t.Fatalf("Input handled incorrectly:\nwant: wait\ngot: %s\nerror: %s\n", cmd, err) - } - // test wait entry with valid quit after - w.WriteString("wq") - if cmd, err := state.getValidInput(); err != nil || cmd != "wait" { - t.Fatalf("Input handled incorrectly:\nwant: wait\ngot: %s\nerror: %s\n", cmd, err) - } - // test input error - w.Close() - if cmd, err := state.getValidInput(); err == nil { - t.Fatalf("Input handled incorrectly:\nwant: wait\ngot: %s\nerror: %s\n", cmd, err) - } -} diff --git a/client/menu.go b/client/menu.go deleted file mode 100644 index f6de082..0000000 --- a/client/menu.go +++ /dev/null @@ -1,17 +0,0 @@ -package client - -import "fmt" - -func (s *State) displayMenu() { - menu := s.config.Menus[s.currentMenu] - fmt.Print("\033[2J") // clear the screen - fmt.Printf("%s\n\n", menu.Banner) - if s.login == "" { - fmt.Print("Not logged in.\n\n") - } else { - fmt.Printf("Logged in as: %s\n\n", s.login) - } - for i := 0; i < len(menu.MenuEntries); i++ { - fmt.Printf("%s) %s\n", menu.MenuEntries[i].Key, menu.MenuEntries[i].Label) - } -} diff --git a/client/menu_test.go b/client/menu_test.go deleted file mode 100644 index 1f70e4e..0000000 --- a/client/menu_test.go +++ /dev/null @@ -1,92 +0,0 @@ -package client - -import ( - "io/ioutil" - "os" - "reflect" - "shell-game-launcher/config" - "testing" -) - -func TestDisplayMenu(t *testing.T) { - realStdout := os.Stdout - t.Cleanup(func() { os.Stdout = realStdout }) - r, w, _ := os.Pipe() - os.Stdout = w - - // Complete menu, while not logged in - state := State{ - config: &config.Config{ - Menus: map[string]config.Menu{ - "test": config.Menu{ - Banner: "TEST TEST TEST", - MenuEntries: []config.MenuEntry{ - config.MenuEntry{ - Key: "q", - Label: "quit entry", - Action: "quit", - }, - }, - }, - }, - }, - currentMenu: "test", - login: "", - } - want := []byte("\033[2J" + - "TEST TEST TEST\n" + - "\n" + - "Not logged in.\n" + - "\n" + - "q) quit entry\n") - state.displayMenu() - // back to normal state - w.Close() - out, _ := ioutil.ReadAll(r) - if !reflect.DeepEqual(out, want) { - t.Fatalf("menu displayed incorrectly:\nwant:%+v\ngot: %+v", want, out) - } - - // Complete menu, while logged in - r, w, _ = os.Pipe() - os.Stdout = w - - // Complete menu, while not logged in - state = State{ - config: &config.Config{ - Menus: map[string]config.Menu{ - "test": config.Menu{ - Banner: "TEST TEST TEST", - MenuEntries: []config.MenuEntry{ - config.MenuEntry{ - Key: "w", - Label: "wait entry", - Action: "wait", - }, - config.MenuEntry{ - Key: "q", - Label: "quit entry", - Action: "quit", - }, - }, - }, - }, - }, - currentMenu: "test", - login: "test", - } - want = []byte("\033[2J" + - "TEST TEST TEST\n" + - "\n" + - "Logged in as: test\n" + - "\n" + - "w) wait entry\n" + - "q) quit entry\n") - state.displayMenu() - // back to normal state - w.Close() - out, _ = ioutil.ReadAll(r) - if !reflect.DeepEqual(out, want) { - t.Fatalf("menu displayed incorrectly:\nwant:%+v\ngot: %+v", want, out) - } -} diff --git a/client/state.go b/client/state.go deleted file mode 100644 index 9565efc..0000000 --- a/client/state.go +++ /dev/null @@ -1,23 +0,0 @@ -package client - -import ( - "shell-game-launcher/config" -) - -type State struct { - config *config.Config - currentMenu string - login string -} - -func NewState(config *config.Config, login string) *State { - cs := State{ - config: config, - currentMenu: "anonymous", - login: login, - } - if login != "" { - cs.currentMenu = "logged_in" - } - return &cs -} diff --git a/client/state_test.go b/client/state_test.go deleted file mode 100644 index 917c211..0000000 --- a/client/state_test.go +++ /dev/null @@ -1,14 +0,0 @@ -package client - -import "testing" - -func TestNewState(t *testing.T) { - // Empty login - if s := NewState(nil, ""); s.currentMenu != "anonymous" { - t.Fatal("a new state without login should init to anonymous") - } - // logged_in - if s := NewState(nil, "test"); s.currentMenu != "logged_in" { - t.Fatal("a new state with login should init to logged_in") - } -} diff --git a/config/action.go b/config/action.go deleted file mode 100644 index c63308e..0000000 --- a/config/action.go +++ /dev/null @@ -1,54 +0,0 @@ -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 deleted file mode 100644 index dbc6ae3..0000000 --- a/config/action_test.go +++ /dev/null @@ -1,105 +0,0 @@ -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 deleted file mode 100644 index 2eefb98..0000000 --- a/config/app.go +++ /dev/null @@ -1,54 +0,0 @@ -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 - WorkingDirectory string `yaml:"WorkingDirectory"` - // MaxUsers is the maximum amount of registered users to allow - MaxUsers int `yaml:"MaxUsers"` - // AllowRegistration allows registration of new users - AllowRegistration bool `yaml:"AllowRegistration"` - // MaxNickLen Maximum length for a nickname - MaxNickLen int `yaml:"MaxNickLen"` - // MenuMaxIdleTime is the maximum number of seconds a user can be idle on the menu before the program exits - MenuMaxIdleTime int `yaml:"MenuMaxIdleTime"` - // 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|unix.R_OK|unix.X_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 deleted file mode 100644 index 5a6cf41..0000000 --- a/config/app_test.go +++ /dev/null @@ -1,90 +0,0 @@ -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 are mostly tested from command_test.go - app = App{ - WorkingDirectory: "var/", - MaxUsers: 512, - MaxNickLen: 15, - MenuMaxIdleTime: 60, - } - if err := app.validate(); err != nil { - t.Fatal("Empty PostLoginCommands list should be valid") - } - app = App{ - WorkingDirectory: "var/", - MaxUsers: 512, - MaxNickLen: 15, - MenuMaxIdleTime: 60, - PostLoginCommands: []string{"invalid"}, - } - if err := app.validate(); err == nil { - t.Fatal("Invalid command in PostLoginCommands should not be valid") - } - - // A valid App - app = App{ - WorkingDirectory: "var/", - MaxUsers: 512, - MaxNickLen: 15, - MenuMaxIdleTime: 60, - PostLoginCommands: []string{"wait"}, - } - if err := app.validate(); err != nil { - t.Fatal("A valid app should pass") - } -} diff --git a/config/command.go b/config/command.go deleted file mode 100644 index 34142cd..0000000 --- a/config/command.go +++ /dev/null @@ -1,32 +0,0 @@ -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 deleted file mode 100644 index 7e07956..0000000 --- a/config/command_test.go +++ /dev/null @@ -1,51 +0,0 @@ -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 deleted file mode 100644 index b68a26a..0000000 --- a/config/config.go +++ /dev/null @@ -1,64 +0,0 @@ -package config - -import ( - "os" - - "github.com/pkg/errors" - "gopkg.in/yaml.v3" -) - -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 map[string]Menu `yaml:"Menus"` - // Games is the list of games. - Games map[string]Game `yaml:"Games"` -} - -func (c *Config) validate() error { - // App - if err := c.App.validate(); err != nil { - return err - } - // Menus - if len(c.Menus) < 2 { - return errors.New("A valid configuration needs at least two menu entries named anonymous and logged_in") - } - for k, v := range c.Menus { - if err := v.validate(k); err != nil { - return err - } - } - // Games - for k, v := range c.Games { - if err := v.validate(k); err != nil { - return err - } - } - return nil -} - -// LoadFile loads the c from a given file -func LoadFile(path string) (*Config, error) { - var c *Config - f, errOpen := os.Open(path) - if errOpen != nil { - return nil, errors.Wrapf(errOpen, "Failed to open configuration file %s", path) - } - defer f.Close() - decoder := yaml.NewDecoder(f) - if err := decoder.Decode(&c); err != nil { - return nil, errors.Wrap(err, "Failed to decode configuration file") - } - if err := c.validate(); err != nil { - return nil, errors.Wrap(err, "Failed to validate configuration") - } - // If all looks good we validate menu consistency - for _, v := range c.Menus { - if err := v.validateConsistency(c); err != nil { - return nil, errors.Wrap(err, "Failed menu consistency checks") - } - } - return c, nil -} diff --git a/config/config_test.go b/config/config_test.go deleted file mode 100644 index dfa8237..0000000 --- a/config/config_test.go +++ /dev/null @@ -1,221 +0,0 @@ -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") - } - - // Minimal yaml file - want := Config{ - App: App{ - WorkingDirectory: "var/", - MaxUsers: 1, - AllowRegistration: true, - MaxNickLen: 15, - MenuMaxIdleTime: 600, - }, - Menus: map[string]Menu{ - "anonymous": Menu{ - MenuEntries: []MenuEntry{ - MenuEntry{ - Key: "q", - Label: "quit", - Action: "quit", - }, - }, - }, - "logged_in": Menu{ - MenuEntries: []MenuEntry{ - MenuEntry{ - Key: "q", - Label: "quit", - Action: "quit", - }, - }, - }, - }, - } - config, err := LoadFile("test_data/minimal.yaml") - if err != nil { - t.Fatalf("minimal example failed with error : %v", err) - } - if config != nil && !reflect.DeepEqual(want, *config) { - t.Fatalf("minimal example failed:\nwant:%+v\ngot: %+v", want, *config) - } - t.Cleanup(func() { os.RemoveAll("var/") }) - // Invalid App - if _, err := LoadFile("test_data/invalid_app.yaml"); err == nil { - t.Fatal("Invalid App entry should fail to load") - } - // Not enough menus - if _, err := LoadFile("test_data/not_enough_menus.yaml"); err == nil { - t.Fatal("not enough menu entries should fail to load") - } - // Invalid Menus - if _, err := LoadFile("test_data/invalid_menus.yaml"); err == nil { - t.Fatal("Invalid menu entry should fail to load") - } - // no anonymous Menu - if _, err := LoadFile("test_data/no_anonymous_menu.yaml"); err == nil { - t.Fatal("Invalid menu entry should fail to load") - } - // no logged_in Menu - if _, err := LoadFile("test_data/no_logged_in_menu.yaml"); err == nil { - t.Fatal("Invalid menu entry should fail to load") - } - // duplicate menu - if _, err := LoadFile("test_data/duplicate_menu.yaml"); err == nil { - t.Fatal("duplicate menu should fail to load") - } - // non existant menu action referenced - if _, err := LoadFile("test_data/non_existant_menu.yaml"); err == nil { - t.Fatal("menu entry referencing a non existant menu should fail to load") - } - // non existant game referenced in play action - if _, err := LoadFile("test_data/non_existant_game.yaml"); err == nil { - t.Fatal("menu entry referencing a non existant play action should fail to load") - } - // unreachable menu - if _, err := LoadFile("test_data/unreachable_menu.yaml"); err == nil { - t.Fatal("unreachable menu should fail to load") - } - // invalid game - if _, err := LoadFile("test_data/invalid_game.yaml"); err == nil { - t.Fatal("invalid game should fail to load") - } - // unreachable game - if _, err := LoadFile("test_data/unreachable_game.yaml"); err == nil { - t.Fatal("unreachable game should fail to load") - } - // duplicate game - if _, err := LoadFile("test_data/duplicate_game.yaml"); err == nil { - t.Fatal("unreachable game should fail to load") - } - - // Complexe example - want = Config{ - App: App{ - WorkingDirectory: "var/", - MaxUsers: 512, - AllowRegistration: true, - MaxNickLen: 15, - MenuMaxIdleTime: 600, - PostLoginCommands: []string{ - "mkdir %w/userdata/%u", - "mkdir %w/userdata/%u/dumplog", - "mkdir %w/userdata/%u/ttyrec", - }, - }, - Menus: map[string]Menu{ - "anonymous": Menu{ - Banner: "Shell Game Launcher - Anonymous access%n======================================", - MenuEntries: []MenuEntry{ - MenuEntry{ - Key: "l", - Label: "login", - Action: "login", - }, - MenuEntry{ - Key: "r", - Label: "register", - Action: "register", - }, - MenuEntry{ - Key: "w", - Label: "watch", - Action: "watch_menu", - }, - MenuEntry{ - Key: "q", - Label: "quit", - Action: "quit", - }, - }, - }, - "logged_in": Menu{ - Banner: "Shell Game Launcher%n===================", - MenuEntries: []MenuEntry{ - MenuEntry{ - Key: "p", - Label: "play Nethack 3.7", - Action: "play nethack3.7", - }, - MenuEntry{ - Key: "o", - Label: "edit game options", - Action: "menu options", - }, - MenuEntry{ - Key: "w", - Label: "watch", - Action: "watch", - }, - MenuEntry{ - Key: "r", - Label: "replay", - Action: "replay", - }, - MenuEntry{ - Key: "c", - Label: "change password", - Action: "passwd", - }, - MenuEntry{ - Key: "m", - Label: "change email", - Action: "chmail", - }, - MenuEntry{ - Key: "q", - Label: "quit", - Action: "quit", - }, - }, - }, - "options": Menu{ - Banner: "Options%n=======", - MenuEntries: []MenuEntry{ - MenuEntry{ - Key: "z", - Label: "back", - Action: "menu logged_in", - }, - }, - }, - }, - Games: map[string]Game{ - "nethack3.7": Game{ - ChrootPath: "test_data/fake_nethack_directory", - FileMode: "0666", - 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", - }, - }, - }, - } - config, err = LoadFile("../example/complete.yaml") - if err != nil { - t.Fatalf("complete example failed with error : %v", err) - } - if config != nil && !reflect.DeepEqual(want, *config) { - t.Fatalf("complete example failed:\nwant:%+v\ngot: %+v", want, *config) - } -} diff --git a/config/game.go b/config/game.go deleted file mode 100644 index b5f4d39..0000000 --- a/config/game.go +++ /dev/null @@ -1,63 +0,0 @@ -package config - -import ( - "regexp" - - "github.com/pkg/errors" - "golang.org/x/sys/unix" -) - -var reValidGameName = regexp.MustCompile(`^[\w\._]+$`) -var reValidFileMode = regexp.MustCompile(`^0[\d]{3}$`) -var reSpace = regexp.MustCompile(`^\s$`) - -// Game struct containers the configuration for a game -type Game struct { - // ChrootPath is the chroot path for the game - ChrootPath string `yaml:"ChrootPath"` - // FileMode is the file mode to use when copying files - FileMode string `yaml:"FileMode"` - // Commands is the command list - Commands []string `yaml:"Commands"` - // Env is the environment in which to exec the commands - Env map[string]string `yaml:"Env"` -} - -func (g *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 - if err := unix.Access(g.ChrootPath, unix.R_OK|unix.X_OK); err != nil { - return errors.Wrapf(err, "Invalid ChrootPath : %s", g.ChrootPath) - } - // FileMode - if ok := reValidFileMode.MatchString(g.FileMode); !ok { - return errors.New("Invalid File Mode, must match regex `^0[\\d]{3}$` : " + name) - } - // Commands - if len(g.Commands) == 0 { - return errors.New("Invalid game " + name + " has no commands") - } - for i := 0; i < len(g.Commands); i++ { - if err := validateCommand(g.Commands[i]); err != nil { - return errors.Wrapf(err, "Failed to validate Commands for game %s", name) - } - } - // Env - for k, _ := range g.Env { - for _, c := range k { - switch c { - case '=': - return errors.New("Environment variable key must not contain equal sign") - case '\000': - return errors.New("Environment variable key must not contain null character") - } - if reSpace.MatchString(string(c)) { - return errors.New("Environment variable key must not contain spaces") - } - } - } - return nil -} diff --git a/config/game_test.go b/config/game_test.go deleted file mode 100644 index b3bf107..0000000 --- a/config/game_test.go +++ /dev/null @@ -1,107 +0,0 @@ -package config - -import "testing" - -func TestGameValidate(t *testing.T) { - // Game name - game := Game{} - if err := game.validate("invalid game name because of spaces"); err == nil { - t.Fatal("game name with spaces should not be valid") - } - // ChrootPath - game = Game{ChrootPath: "test_data/non_existant"} - if err := game.validate("test"); err == nil { - t.Fatal("non_existant ChrootPath should not be valid") - } - // FileMode - game = Game{ - ChrootPath: "test_data/fake_nethack_directory", - } - if err := game.validate("test"); err == nil { - t.Fatal("Invalid FileMode should not be valid") - } - game = Game{ - ChrootPath: "test_data/fake_nethack_directory", - FileMode: "abcd", - } - if err := game.validate("test"); err == nil { - t.Fatal("Invalid FileMode should not be valid") - } - game = Game{ - ChrootPath: "test_data/fake_nethack_directory", - FileMode: "777", - } - if err := game.validate("test"); err == nil { - t.Fatal("Invalid FileMode should not be valid") - } - // Commands are mostly tested from command_test.go - game = Game{ - ChrootPath: "test_data/fake_nethack_directory", - FileMode: "0777", - } - if err := game.validate("test"); err == nil { - t.Fatal("Empty Commands list should not be valid") - } - game = Game{ - ChrootPath: "test_data/fake_nethack_directory", - FileMode: "0777", - Commands: []string{"invalid"}, - } - if err := game.validate("test"); err == nil { - t.Fatal("Invalid command in Commands should not be valid") - } - // Env - game = Game{ - ChrootPath: "test_data/fake_nethack_directory", - FileMode: "0777", - Commands: []string{"wait"}, - } - if err := game.validate("test"); err != nil { - t.Fatal("Empty env list should be valid") - } - game = Game{ - ChrootPath: "test_data/fake_nethack_directory", - FileMode: "0777", - Commands: []string{"wait"}, - Env: map[string]string{ - "test invalid": "test", - }, - } - if err := game.validate("test"); err == nil { - t.Fatal("Spaces in environnement variable name are invalid") - } - game = Game{ - ChrootPath: "test_data/fake_nethack_directory", - FileMode: "0777", - Commands: []string{"wait"}, - Env: map[string]string{ - "test\000invalid": "test", - }, - } - if err := game.validate("test"); err == nil { - t.Fatal("null character in environnement variable name are invalid") - } - game = Game{ - ChrootPath: "test_data/fake_nethack_directory", - FileMode: "0777", - Commands: []string{"wait"}, - Env: map[string]string{ - "test=invalid": "test", - }, - } - if err := game.validate("test"); err == nil { - t.Fatal("equals symbol in environnement variable name are invalid") - } - // Valid Game entry - game = Game{ - ChrootPath: "test_data/fake_nethack_directory", - FileMode: "0777", - Commands: []string{"wait"}, - Env: map[string]string{ - "test": "test", - }, - } - if err := game.validate("test"); err != nil { - t.Fatalf("Valid game entry should pass but got error %s", err) - } -} diff --git a/config/menu.go b/config/menu.go deleted file mode 100644 index 7321c7b..0000000 --- a/config/menu.go +++ /dev/null @@ -1,113 +0,0 @@ -package config - -import ( - "regexp" - "strings" - - "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 - Banner string `yaml:"Banner"` - // Commands is the list of commands in the menu - MenuEntries []MenuEntry `yaml:"MenuEntries"` -} - -// MenuEntry struct describes a menu entry -type MenuEntry struct { - // Key is the key associated with the action. We need to store it as a string because of how yaml unmarshal works - Key string `yaml:"Key"` - // Label is the text displayed on the menu - Label string `yaml:"Label"` - // 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 - // MenuEntries - if len(m.MenuEntries) == 0 { - return errors.New("A Menu needs MenuEntries to be valid") - } - // Duplicate detection is natively handled by the yaml parser - for i := 0; i < len(m.MenuEntries); i++ { - m.MenuEntries[i].validate() - if m.MenuEntries[i].Action == "menu "+name { - return errors.New("A menu shall not loop on itself") - } - } - // Loop test - return nil -} - -func (m *Menu) validateConsistency(c *Config) error { - // Necessary menus - if _, ok := c.Menus["anonymous"]; !ok { - return errors.New("No anonymous menu declared") - } - if _, ok := c.Menus["logged_in"]; !ok { - return errors.New("No logged_in menu declared") - } - // Validate actions - menus := map[string]bool{ - "anonymous": true, - "logged_in": true, - } - playable := make(map[string]bool) - for k, v := range c.Menus { - for _, e := range v.MenuEntries { - tokens := strings.Split(e.Action, " ") - switch tokens[0] { - case "menu": - if _, ok := c.Menus[tokens[1]]; ok { - menus[tokens[1]] = true - } else { - return errors.New("menu action " + tokens[1] + " in menu " + k + " does not exist") - } - case "play": - if _, ok := c.Games[tokens[1]]; ok { - playable[tokens[1]] = true - } else { - return errors.New("play action " + tokens[1] + " in menu " + k + " does not exist") - } - } - } - } - // Check for unreachables - for k, _ := range c.Menus { - if _, ok := menus[k]; !ok { - return errors.New("unreachable menu : " + k) - } - } - for k, _ := range c.Games { - if _, ok := playable[k]; !ok { - return errors.New("unplayable game : " + k) - } - } - 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 deleted file mode 100644 index e9abef1..0000000 --- a/config/menu_test.go +++ /dev/null @@ -1,71 +0,0 @@ -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 - // MenuEntries are mostly tested bellow - menu = Menu{} - if err := menu.validate("test"); err == nil { - t.Fatal("A menu without menu entries should not be valid") - } - // loop menu - menu = Menu{ - 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{ - 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/duplicate_game.yaml b/config/test_data/duplicate_game.yaml deleted file mode 100644 index f01a017..0000000 --- a/config/test_data/duplicate_game.yaml +++ /dev/null @@ -1,30 +0,0 @@ -App: - WorkingDirectory: var/ - MaxUsers: 1 - AllowRegistration: true - MaxNickLen: 15 - MenuMaxIdleTime: 600 - -Menus: - anonymous: - MenuEntries: - - Key: q - Label: quit - Action: quit - logged_in: - MenuEntries: - - Key: p - Label: play - Action: play test - -Games: - test: - ChrootPath: test_data/fake_nethack_directory - FileMode: 0777 - Commands: - - wait - test: - ChrootPath: test_data/fake_nethack_directory - FileMode: 0777 - Commands: - - wait diff --git a/config/test_data/duplicate_menu.yaml b/config/test_data/duplicate_menu.yaml deleted file mode 100644 index 3dbefb7..0000000 --- a/config/test_data/duplicate_menu.yaml +++ /dev/null @@ -1,28 +0,0 @@ -App: - WorkingDirectory: var/ - MaxUsers: 1 - AllowRegistration: true - MaxNickLen: 15 - MenuMaxIdleTime: 600 - -Menus: - anonymous: - MenuEntries: - - Key: t - Label: test - Action: menu test - logged_in: - MenuEntries: - - Key: q - Label: quit - Action: quit - test: - MenuEntries: - - Key: q - Label: quit - Action: quit - test: - MenuEntries: - - Key: a - Label: login - Action: login diff --git a/config/test_data/fake_nethack_directory/.keep b/config/test_data/fake_nethack_directory/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/config/test_data/invalid_app.yaml b/config/test_data/invalid_app.yaml deleted file mode 100644 index ed236ea..0000000 --- a/config/test_data/invalid_app.yaml +++ /dev/null @@ -1,11 +0,0 @@ -Menus: - anonymous: - MenuEntries: - - Key: q - Label: quit - Action: quit - logged_in: - MenuEntries: - - Key: q - Label: quit - Action: quit diff --git a/config/test_data/invalid_game.yaml b/config/test_data/invalid_game.yaml deleted file mode 100644 index d58c3ee..0000000 --- a/config/test_data/invalid_game.yaml +++ /dev/null @@ -1,21 +0,0 @@ -App: - WorkingDirectory: var/ - MaxUsers: 1 - AllowRegistration: true - MaxNickLen: 15 - MenuMaxIdleTime: 600 - -Menus: - anonymous: - MenuEntries: - - Key: q - Label: quit - Action: quit - logged_in: - MenuEntries: - - Key: p - Label: play - Action: play test - -Games: - test: diff --git a/config/test_data/invalid_menus.yaml b/config/test_data/invalid_menus.yaml deleted file mode 100644 index 1df5fbf..0000000 --- a/config/test_data/invalid_menus.yaml +++ /dev/null @@ -1,19 +0,0 @@ -App: - WorkingDirectory: var/ - MaxUsers: 1 - AllowRegistration: true - MaxNickLen: 15 - MenuMaxIdleTime: 600 - -Menus: - anonymous: - MenuEntries: - - Key: q - Label: quit - Action: quit - logged_in: - MenuEntries: - - Key: q - Label: quit - Action: quit - test: diff --git a/config/test_data/invalid_yaml b/config/test_data/invalid_yaml deleted file mode 100644 index db1ddad..0000000 --- a/config/test_data/invalid_yaml +++ /dev/null @@ -1 +0,0 @@ -blargh(ads) diff --git a/config/test_data/minimal.yaml b/config/test_data/minimal.yaml deleted file mode 100644 index 22a0a6c..0000000 --- a/config/test_data/minimal.yaml +++ /dev/null @@ -1,18 +0,0 @@ -App: - WorkingDirectory: var/ - MaxUsers: 1 - AllowRegistration: true - MaxNickLen: 15 - MenuMaxIdleTime: 600 - -Menus: - anonymous: - MenuEntries: - - Key: q - Label: quit - Action: quit - logged_in: - MenuEntries: - - Key: q - Label: quit - Action: quit diff --git a/config/test_data/no_anonymous_menu.yaml b/config/test_data/no_anonymous_menu.yaml deleted file mode 100644 index a015160..0000000 --- a/config/test_data/no_anonymous_menu.yaml +++ /dev/null @@ -1,18 +0,0 @@ -App: - WorkingDirectory: var/ - MaxUsers: 1 - AllowRegistration: true - MaxNickLen: 15 - MenuMaxIdleTime: 600 - -Menus: - test: - MenuEntries: - - Key: q - Label: quit - Action: quit - logged_in: - MenuEntries: - - 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 deleted file mode 100644 index 43d0054..0000000 --- a/config/test_data/no_logged_in_menu.yaml +++ /dev/null @@ -1,18 +0,0 @@ -App: - WorkingDirectory: var/ - MaxUsers: 1 - AllowRegistration: true - MaxNickLen: 15 - MenuMaxIdleTime: 600 - -Menus: - anonymous: - MenuEntries: - - Key: q - Label: quit - Action: quit - test: - MenuEntries: - - Key: q - Label: quit - Action: quit diff --git a/config/test_data/non_existant_chopts.yaml b/config/test_data/non_existant_chopts.yaml deleted file mode 100644 index d3f796d..0000000 --- a/config/test_data/non_existant_chopts.yaml +++ /dev/null @@ -1,18 +0,0 @@ -App: - WorkingDirectory: var/ - MaxUsers: 1 - AllowRegistration: true - MaxNickLen: 15 - MenuMaxIdleTime: 600 - -Menus: - anonymous: - MenuEntries: - - Key: q - Label: quit - Action: quit - logged_in: - MenuEntries: - - Key: t - Label: test - Action: chopts invalid diff --git a/config/test_data/non_existant_game.yaml b/config/test_data/non_existant_game.yaml deleted file mode 100644 index 9bf6a38..0000000 --- a/config/test_data/non_existant_game.yaml +++ /dev/null @@ -1,18 +0,0 @@ -App: - WorkingDirectory: var/ - MaxUsers: 1 - AllowRegistration: true - MaxNickLen: 15 - MenuMaxIdleTime: 600 - -Menus: - anonymous: - MenuEntries: - - Key: q - Label: quit - Action: quit - logged_in: - MenuEntries: - - Key: t - Label: test - Action: play invalid diff --git a/config/test_data/non_existant_menu.yaml b/config/test_data/non_existant_menu.yaml deleted file mode 100644 index f0f30a3..0000000 --- a/config/test_data/non_existant_menu.yaml +++ /dev/null @@ -1,18 +0,0 @@ -App: - WorkingDirectory: var/ - MaxUsers: 1 - AllowRegistration: true - MaxNickLen: 15 - MenuMaxIdleTime: 600 - -Menus: - anonymous: - MenuEntries: - - Key: t - Label: test - Action: menu invalid - logged_in: - MenuEntries: - - Key: q - Label: quit - Action: quit diff --git a/config/test_data/not_enough_menus.yaml b/config/test_data/not_enough_menus.yaml deleted file mode 100644 index f585f44..0000000 --- a/config/test_data/not_enough_menus.yaml +++ /dev/null @@ -1,13 +0,0 @@ -App: - WorkingDirectory: var/ - MaxUsers: 1 - AllowRegistration: true - MaxNickLen: 15 - MenuMaxIdleTime: 600 - -Menus: - test: - MenuEntries: - - Key: q - Label: quit - Action: quit diff --git a/config/test_data/unreachable_game.yaml b/config/test_data/unreachable_game.yaml deleted file mode 100644 index f2f22e2..0000000 --- a/config/test_data/unreachable_game.yaml +++ /dev/null @@ -1,25 +0,0 @@ -App: - WorkingDirectory: var/ - MaxUsers: 1 - AllowRegistration: true - MaxNickLen: 15 - MenuMaxIdleTime: 600 - -Menus: - anonymous: - MenuEntries: - - Key: q - Label: quit - Action: quit - logged_in: - MenuEntries: - - Key: q - Label: quit - Action: quit - -Games: - unreachable: - ChrootPath: test_data/fake_nethack_directory - FileMode: 0777 - Commands: - - wait diff --git a/config/test_data/unreachable_menu.yaml b/config/test_data/unreachable_menu.yaml deleted file mode 100644 index f947cf7..0000000 --- a/config/test_data/unreachable_menu.yaml +++ /dev/null @@ -1,23 +0,0 @@ -App: - WorkingDirectory: var/ - MaxUsers: 1 - AllowRegistration: true - MaxNickLen: 15 - MenuMaxIdleTime: 600 - -Menus: - anonymous: - MenuEntries: - - Key: q - Label: quit - Action: quit - logged_in: - MenuEntries: - - Key: q - Label: quit - Action: quit - test: - MenuEntries: - - Key: q - Label: quit - Action: quit diff --git a/pkg/client/input.go b/pkg/client/input.go new file mode 100644 index 0000000..8c814a6 --- /dev/null +++ b/pkg/client/input.go @@ -0,0 +1,26 @@ +package client + +import ( + "bufio" + "os" + + "github.com/pkg/errors" +) + +// getValidInput returns the selected menu command as a string or an error +func (s *State) getValidInput() (string, error) { + menu := s.config.Menus[s.currentMenu] + + reader := bufio.NewReader(os.Stdin) + for { + input, err := reader.ReadByte() + if err != nil { + return "", errors.Wrapf(err, "Could not read byte from stdin") + } + for _, menuEntry := range menu.MenuEntries { + if []byte(menuEntry.Key)[0] == input { + return menuEntry.Action, nil + } + } + } +} diff --git a/pkg/client/input_test.go b/pkg/client/input_test.go new file mode 100644 index 0000000..f74a097 --- /dev/null +++ b/pkg/client/input_test.go @@ -0,0 +1,60 @@ +package client + +import ( + "os" + "shell-game-launcher/pkg/config" + "testing" +) + +func TestGetValidInput(t *testing.T) { + realStdin := os.Stdin + t.Cleanup(func() { os.Stdin = realStdin }) + + // Complete menu, no input error + state := State{ + config: &config.Config{ + Menus: map[string]config.Menu{ + "test": config.Menu{ + Banner: "TEST TEST TEST", + MenuEntries: []config.MenuEntry{ + config.MenuEntry{ + Key: "w", + Label: "wait entry", + Action: "wait", + }, + config.MenuEntry{ + Key: "q", + Label: "quit entry", + Action: "quit", + }, + }, + }, + }, + }, + currentMenu: "test", + login: "", + } + r, w, _ := os.Pipe() + os.Stdin = r + + // Simply test quit entry + w.WriteString("q") + if cmd, err := state.getValidInput(); err != nil || cmd != "quit" { + t.Fatalf("Input handled incorrectly:\nwant: wait\ngot: %s\nerror: %s\n", cmd, err) + } + // test quit entry after wrong keys + w.WriteString("abcdq") + if cmd, err := state.getValidInput(); err != nil || cmd != "quit" { + t.Fatalf("Input handled incorrectly:\nwant: wait\ngot: %s\nerror: %s\n", cmd, err) + } + // test wait entry with valid quit after + w.WriteString("wq") + if cmd, err := state.getValidInput(); err != nil || cmd != "wait" { + t.Fatalf("Input handled incorrectly:\nwant: wait\ngot: %s\nerror: %s\n", cmd, err) + } + // test input error + w.Close() + if cmd, err := state.getValidInput(); err == nil { + t.Fatalf("Input handled incorrectly:\nwant: wait\ngot: %s\nerror: %s\n", cmd, err) + } +} diff --git a/pkg/client/menu.go b/pkg/client/menu.go new file mode 100644 index 0000000..f6de082 --- /dev/null +++ b/pkg/client/menu.go @@ -0,0 +1,17 @@ +package client + +import "fmt" + +func (s *State) displayMenu() { + menu := s.config.Menus[s.currentMenu] + fmt.Print("\033[2J") // clear the screen + fmt.Printf("%s\n\n", menu.Banner) + if s.login == "" { + fmt.Print("Not logged in.\n\n") + } else { + fmt.Printf("Logged in as: %s\n\n", s.login) + } + for i := 0; i < len(menu.MenuEntries); i++ { + fmt.Printf("%s) %s\n", menu.MenuEntries[i].Key, menu.MenuEntries[i].Label) + } +} diff --git a/pkg/client/menu_test.go b/pkg/client/menu_test.go new file mode 100644 index 0000000..35964d8 --- /dev/null +++ b/pkg/client/menu_test.go @@ -0,0 +1,92 @@ +package client + +import ( + "io/ioutil" + "os" + "reflect" + "shell-game-launcher/pkg/config" + "testing" +) + +func TestDisplayMenu(t *testing.T) { + realStdout := os.Stdout + t.Cleanup(func() { os.Stdout = realStdout }) + r, w, _ := os.Pipe() + os.Stdout = w + + // Complete menu, while not logged in + state := State{ + config: &config.Config{ + Menus: map[string]config.Menu{ + "test": config.Menu{ + Banner: "TEST TEST TEST", + MenuEntries: []config.MenuEntry{ + config.MenuEntry{ + Key: "q", + Label: "quit entry", + Action: "quit", + }, + }, + }, + }, + }, + currentMenu: "test", + login: "", + } + want := []byte("\033[2J" + + "TEST TEST TEST\n" + + "\n" + + "Not logged in.\n" + + "\n" + + "q) quit entry\n") + state.displayMenu() + // back to normal state + w.Close() + out, _ := ioutil.ReadAll(r) + if !reflect.DeepEqual(out, want) { + t.Fatalf("menu displayed incorrectly:\nwant:%+v\ngot: %+v", want, out) + } + + // Complete menu, while logged in + r, w, _ = os.Pipe() + os.Stdout = w + + // Complete menu, while not logged in + state = State{ + config: &config.Config{ + Menus: map[string]config.Menu{ + "test": config.Menu{ + Banner: "TEST TEST TEST", + MenuEntries: []config.MenuEntry{ + config.MenuEntry{ + Key: "w", + Label: "wait entry", + Action: "wait", + }, + config.MenuEntry{ + Key: "q", + Label: "quit entry", + Action: "quit", + }, + }, + }, + }, + }, + currentMenu: "test", + login: "test", + } + want = []byte("\033[2J" + + "TEST TEST TEST\n" + + "\n" + + "Logged in as: test\n" + + "\n" + + "w) wait entry\n" + + "q) quit entry\n") + state.displayMenu() + // back to normal state + w.Close() + out, _ = ioutil.ReadAll(r) + if !reflect.DeepEqual(out, want) { + t.Fatalf("menu displayed incorrectly:\nwant:%+v\ngot: %+v", want, out) + } +} diff --git a/pkg/client/state.go b/pkg/client/state.go new file mode 100644 index 0000000..576721c --- /dev/null +++ b/pkg/client/state.go @@ -0,0 +1,23 @@ +package client + +import ( + "shell-game-launcher/pkg/config" +) + +type State struct { + config *config.Config + currentMenu string + login string +} + +func NewState(config *config.Config, login string) *State { + cs := State{ + config: config, + currentMenu: "anonymous", + login: login, + } + if login != "" { + cs.currentMenu = "logged_in" + } + return &cs +} diff --git a/pkg/client/state_test.go b/pkg/client/state_test.go new file mode 100644 index 0000000..917c211 --- /dev/null +++ b/pkg/client/state_test.go @@ -0,0 +1,14 @@ +package client + +import "testing" + +func TestNewState(t *testing.T) { + // Empty login + if s := NewState(nil, ""); s.currentMenu != "anonymous" { + t.Fatal("a new state without login should init to anonymous") + } + // logged_in + if s := NewState(nil, "test"); s.currentMenu != "logged_in" { + t.Fatal("a new state with login should init to logged_in") + } +} diff --git a/pkg/config/action.go b/pkg/config/action.go new file mode 100644 index 0000000..c63308e --- /dev/null +++ b/pkg/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/pkg/config/action_test.go b/pkg/config/action_test.go new file mode 100644 index 0000000..dbc6ae3 --- /dev/null +++ b/pkg/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/pkg/config/app.go b/pkg/config/app.go new file mode 100644 index 0000000..2787517 --- /dev/null +++ b/pkg/config/app.go @@ -0,0 +1,55 @@ +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 + WorkingDirectory string `yaml:"WorkingDirectory"` + // MaxUsers is the maximum amount of registered users to allow + MaxUsers int `yaml:"MaxUsers"` + // AllowRegistration allows registration of new users + AllowRegistration bool `yaml:"AllowRegistration"` + // MaxNickLen Maximum length for a nickname + MaxNickLen int `yaml:"MaxNickLen"` + // MenuMaxIdleTime is the maximum number of seconds a user can be idle on the menu before the program exits + MenuMaxIdleTime int `yaml:"MenuMaxIdleTime"` + // PostLoginCommands is the list of commands to execute upon login, like creating save directories for games + PostLoginCommands []string `yaml:"PostLoginCommands"` + // TODO admin contact +} + +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|unix.R_OK|unix.X_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/pkg/config/app_test.go b/pkg/config/app_test.go new file mode 100644 index 0000000..5a6cf41 --- /dev/null +++ b/pkg/config/app_test.go @@ -0,0 +1,90 @@ +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 are mostly tested from command_test.go + app = App{ + WorkingDirectory: "var/", + MaxUsers: 512, + MaxNickLen: 15, + MenuMaxIdleTime: 60, + } + if err := app.validate(); err != nil { + t.Fatal("Empty PostLoginCommands list should be valid") + } + app = App{ + WorkingDirectory: "var/", + MaxUsers: 512, + MaxNickLen: 15, + MenuMaxIdleTime: 60, + PostLoginCommands: []string{"invalid"}, + } + if err := app.validate(); err == nil { + t.Fatal("Invalid command in PostLoginCommands should not be valid") + } + + // A valid App + app = App{ + WorkingDirectory: "var/", + MaxUsers: 512, + MaxNickLen: 15, + MenuMaxIdleTime: 60, + PostLoginCommands: []string{"wait"}, + } + if err := app.validate(); err != nil { + t.Fatal("A valid app should pass") + } +} diff --git a/pkg/config/command.go b/pkg/config/command.go new file mode 100644 index 0000000..34142cd --- /dev/null +++ b/pkg/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/pkg/config/command_test.go b/pkg/config/command_test.go new file mode 100644 index 0000000..7e07956 --- /dev/null +++ b/pkg/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/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..b68a26a --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,64 @@ +package config + +import ( + "os" + + "github.com/pkg/errors" + "gopkg.in/yaml.v3" +) + +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 map[string]Menu `yaml:"Menus"` + // Games is the list of games. + Games map[string]Game `yaml:"Games"` +} + +func (c *Config) validate() error { + // App + if err := c.App.validate(); err != nil { + return err + } + // Menus + if len(c.Menus) < 2 { + return errors.New("A valid configuration needs at least two menu entries named anonymous and logged_in") + } + for k, v := range c.Menus { + if err := v.validate(k); err != nil { + return err + } + } + // Games + for k, v := range c.Games { + if err := v.validate(k); err != nil { + return err + } + } + return nil +} + +// LoadFile loads the c from a given file +func LoadFile(path string) (*Config, error) { + var c *Config + f, errOpen := os.Open(path) + if errOpen != nil { + return nil, errors.Wrapf(errOpen, "Failed to open configuration file %s", path) + } + defer f.Close() + decoder := yaml.NewDecoder(f) + if err := decoder.Decode(&c); err != nil { + return nil, errors.Wrap(err, "Failed to decode configuration file") + } + if err := c.validate(); err != nil { + return nil, errors.Wrap(err, "Failed to validate configuration") + } + // If all looks good we validate menu consistency + for _, v := range c.Menus { + if err := v.validateConsistency(c); err != nil { + return nil, errors.Wrap(err, "Failed menu consistency checks") + } + } + return c, nil +} diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go new file mode 100644 index 0000000..8eb6598 --- /dev/null +++ b/pkg/config/config_test.go @@ -0,0 +1,221 @@ +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") + } + + // Minimal yaml file + want := Config{ + App: App{ + WorkingDirectory: "var/", + MaxUsers: 1, + AllowRegistration: true, + MaxNickLen: 15, + MenuMaxIdleTime: 600, + }, + Menus: map[string]Menu{ + "anonymous": Menu{ + MenuEntries: []MenuEntry{ + MenuEntry{ + Key: "q", + Label: "quit", + Action: "quit", + }, + }, + }, + "logged_in": Menu{ + MenuEntries: []MenuEntry{ + MenuEntry{ + Key: "q", + Label: "quit", + Action: "quit", + }, + }, + }, + }, + } + config, err := LoadFile("test_data/minimal.yaml") + if err != nil { + t.Fatalf("minimal example failed with error : %v", err) + } + if config != nil && !reflect.DeepEqual(want, *config) { + t.Fatalf("minimal example failed:\nwant:%+v\ngot: %+v", want, *config) + } + t.Cleanup(func() { os.RemoveAll("var/") }) + // Invalid App + if _, err := LoadFile("test_data/invalid_app.yaml"); err == nil { + t.Fatal("Invalid App entry should fail to load") + } + // Not enough menus + if _, err := LoadFile("test_data/not_enough_menus.yaml"); err == nil { + t.Fatal("not enough menu entries should fail to load") + } + // Invalid Menus + if _, err := LoadFile("test_data/invalid_menus.yaml"); err == nil { + t.Fatal("Invalid menu entry should fail to load") + } + // no anonymous Menu + if _, err := LoadFile("test_data/no_anonymous_menu.yaml"); err == nil { + t.Fatal("Invalid menu entry should fail to load") + } + // no logged_in Menu + if _, err := LoadFile("test_data/no_logged_in_menu.yaml"); err == nil { + t.Fatal("Invalid menu entry should fail to load") + } + // duplicate menu + if _, err := LoadFile("test_data/duplicate_menu.yaml"); err == nil { + t.Fatal("duplicate menu should fail to load") + } + // non existant menu action referenced + if _, err := LoadFile("test_data/non_existant_menu.yaml"); err == nil { + t.Fatal("menu entry referencing a non existant menu should fail to load") + } + // non existant game referenced in play action + if _, err := LoadFile("test_data/non_existant_game.yaml"); err == nil { + t.Fatal("menu entry referencing a non existant play action should fail to load") + } + // unreachable menu + if _, err := LoadFile("test_data/unreachable_menu.yaml"); err == nil { + t.Fatal("unreachable menu should fail to load") + } + // invalid game + if _, err := LoadFile("test_data/invalid_game.yaml"); err == nil { + t.Fatal("invalid game should fail to load") + } + // unreachable game + if _, err := LoadFile("test_data/unreachable_game.yaml"); err == nil { + t.Fatal("unreachable game should fail to load") + } + // duplicate game + if _, err := LoadFile("test_data/duplicate_game.yaml"); err == nil { + t.Fatal("unreachable game should fail to load") + } + + // Complexe example + want = Config{ + App: App{ + WorkingDirectory: "var/", + MaxUsers: 512, + AllowRegistration: true, + MaxNickLen: 15, + MenuMaxIdleTime: 600, + PostLoginCommands: []string{ + "mkdir %w/userdata/%u", + "mkdir %w/userdata/%u/dumplog", + "mkdir %w/userdata/%u/ttyrec", + }, + }, + Menus: map[string]Menu{ + "anonymous": Menu{ + Banner: "Shell Game Launcher - Anonymous access%n======================================", + MenuEntries: []MenuEntry{ + MenuEntry{ + Key: "l", + Label: "login", + Action: "login", + }, + MenuEntry{ + Key: "r", + Label: "register", + Action: "register", + }, + MenuEntry{ + Key: "w", + Label: "watch", + Action: "watch_menu", + }, + MenuEntry{ + Key: "q", + Label: "quit", + Action: "quit", + }, + }, + }, + "logged_in": Menu{ + Banner: "Shell Game Launcher%n===================", + MenuEntries: []MenuEntry{ + MenuEntry{ + Key: "p", + Label: "play Nethack 3.7", + Action: "play nethack3.7", + }, + MenuEntry{ + Key: "o", + Label: "edit game options", + Action: "menu options", + }, + MenuEntry{ + Key: "w", + Label: "watch", + Action: "watch", + }, + MenuEntry{ + Key: "r", + Label: "replay", + Action: "replay", + }, + MenuEntry{ + Key: "c", + Label: "change password", + Action: "passwd", + }, + MenuEntry{ + Key: "m", + Label: "change email", + Action: "chmail", + }, + MenuEntry{ + Key: "q", + Label: "quit", + Action: "quit", + }, + }, + }, + "options": Menu{ + Banner: "Options%n=======", + MenuEntries: []MenuEntry{ + MenuEntry{ + Key: "z", + Label: "back", + Action: "menu logged_in", + }, + }, + }, + }, + Games: map[string]Game{ + "nethack3.7": Game{ + ChrootPath: "test_data/fake_nethack_directory", + FileMode: "0666", + 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", + }, + }, + }, + } + config, err = LoadFile("../../example/complete.yaml") + if err != nil { + t.Fatalf("complete example failed with error : %v", err) + } + if config != nil && !reflect.DeepEqual(want, *config) { + t.Fatalf("complete example failed:\nwant:%+v\ngot: %+v", want, *config) + } +} diff --git a/pkg/config/game.go b/pkg/config/game.go new file mode 100644 index 0000000..b5f4d39 --- /dev/null +++ b/pkg/config/game.go @@ -0,0 +1,63 @@ +package config + +import ( + "regexp" + + "github.com/pkg/errors" + "golang.org/x/sys/unix" +) + +var reValidGameName = regexp.MustCompile(`^[\w\._]+$`) +var reValidFileMode = regexp.MustCompile(`^0[\d]{3}$`) +var reSpace = regexp.MustCompile(`^\s$`) + +// Game struct containers the configuration for a game +type Game struct { + // ChrootPath is the chroot path for the game + ChrootPath string `yaml:"ChrootPath"` + // FileMode is the file mode to use when copying files + FileMode string `yaml:"FileMode"` + // Commands is the command list + Commands []string `yaml:"Commands"` + // Env is the environment in which to exec the commands + Env map[string]string `yaml:"Env"` +} + +func (g *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 + if err := unix.Access(g.ChrootPath, unix.R_OK|unix.X_OK); err != nil { + return errors.Wrapf(err, "Invalid ChrootPath : %s", g.ChrootPath) + } + // FileMode + if ok := reValidFileMode.MatchString(g.FileMode); !ok { + return errors.New("Invalid File Mode, must match regex `^0[\\d]{3}$` : " + name) + } + // Commands + if len(g.Commands) == 0 { + return errors.New("Invalid game " + name + " has no commands") + } + for i := 0; i < len(g.Commands); i++ { + if err := validateCommand(g.Commands[i]); err != nil { + return errors.Wrapf(err, "Failed to validate Commands for game %s", name) + } + } + // Env + for k, _ := range g.Env { + for _, c := range k { + switch c { + case '=': + return errors.New("Environment variable key must not contain equal sign") + case '\000': + return errors.New("Environment variable key must not contain null character") + } + if reSpace.MatchString(string(c)) { + return errors.New("Environment variable key must not contain spaces") + } + } + } + return nil +} diff --git a/pkg/config/game_test.go b/pkg/config/game_test.go new file mode 100644 index 0000000..b3bf107 --- /dev/null +++ b/pkg/config/game_test.go @@ -0,0 +1,107 @@ +package config + +import "testing" + +func TestGameValidate(t *testing.T) { + // Game name + game := Game{} + if err := game.validate("invalid game name because of spaces"); err == nil { + t.Fatal("game name with spaces should not be valid") + } + // ChrootPath + game = Game{ChrootPath: "test_data/non_existant"} + if err := game.validate("test"); err == nil { + t.Fatal("non_existant ChrootPath should not be valid") + } + // FileMode + game = Game{ + ChrootPath: "test_data/fake_nethack_directory", + } + if err := game.validate("test"); err == nil { + t.Fatal("Invalid FileMode should not be valid") + } + game = Game{ + ChrootPath: "test_data/fake_nethack_directory", + FileMode: "abcd", + } + if err := game.validate("test"); err == nil { + t.Fatal("Invalid FileMode should not be valid") + } + game = Game{ + ChrootPath: "test_data/fake_nethack_directory", + FileMode: "777", + } + if err := game.validate("test"); err == nil { + t.Fatal("Invalid FileMode should not be valid") + } + // Commands are mostly tested from command_test.go + game = Game{ + ChrootPath: "test_data/fake_nethack_directory", + FileMode: "0777", + } + if err := game.validate("test"); err == nil { + t.Fatal("Empty Commands list should not be valid") + } + game = Game{ + ChrootPath: "test_data/fake_nethack_directory", + FileMode: "0777", + Commands: []string{"invalid"}, + } + if err := game.validate("test"); err == nil { + t.Fatal("Invalid command in Commands should not be valid") + } + // Env + game = Game{ + ChrootPath: "test_data/fake_nethack_directory", + FileMode: "0777", + Commands: []string{"wait"}, + } + if err := game.validate("test"); err != nil { + t.Fatal("Empty env list should be valid") + } + game = Game{ + ChrootPath: "test_data/fake_nethack_directory", + FileMode: "0777", + Commands: []string{"wait"}, + Env: map[string]string{ + "test invalid": "test", + }, + } + if err := game.validate("test"); err == nil { + t.Fatal("Spaces in environnement variable name are invalid") + } + game = Game{ + ChrootPath: "test_data/fake_nethack_directory", + FileMode: "0777", + Commands: []string{"wait"}, + Env: map[string]string{ + "test\000invalid": "test", + }, + } + if err := game.validate("test"); err == nil { + t.Fatal("null character in environnement variable name are invalid") + } + game = Game{ + ChrootPath: "test_data/fake_nethack_directory", + FileMode: "0777", + Commands: []string{"wait"}, + Env: map[string]string{ + "test=invalid": "test", + }, + } + if err := game.validate("test"); err == nil { + t.Fatal("equals symbol in environnement variable name are invalid") + } + // Valid Game entry + game = Game{ + ChrootPath: "test_data/fake_nethack_directory", + FileMode: "0777", + Commands: []string{"wait"}, + Env: map[string]string{ + "test": "test", + }, + } + if err := game.validate("test"); err != nil { + t.Fatalf("Valid game entry should pass but got error %s", err) + } +} diff --git a/pkg/config/menu.go b/pkg/config/menu.go new file mode 100644 index 0000000..7321c7b --- /dev/null +++ b/pkg/config/menu.go @@ -0,0 +1,113 @@ +package config + +import ( + "regexp" + "strings" + + "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 + Banner string `yaml:"Banner"` + // Commands is the list of commands in the menu + MenuEntries []MenuEntry `yaml:"MenuEntries"` +} + +// MenuEntry struct describes a menu entry +type MenuEntry struct { + // Key is the key associated with the action. We need to store it as a string because of how yaml unmarshal works + Key string `yaml:"Key"` + // Label is the text displayed on the menu + Label string `yaml:"Label"` + // 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 + // MenuEntries + if len(m.MenuEntries) == 0 { + return errors.New("A Menu needs MenuEntries to be valid") + } + // Duplicate detection is natively handled by the yaml parser + for i := 0; i < len(m.MenuEntries); i++ { + m.MenuEntries[i].validate() + if m.MenuEntries[i].Action == "menu "+name { + return errors.New("A menu shall not loop on itself") + } + } + // Loop test + return nil +} + +func (m *Menu) validateConsistency(c *Config) error { + // Necessary menus + if _, ok := c.Menus["anonymous"]; !ok { + return errors.New("No anonymous menu declared") + } + if _, ok := c.Menus["logged_in"]; !ok { + return errors.New("No logged_in menu declared") + } + // Validate actions + menus := map[string]bool{ + "anonymous": true, + "logged_in": true, + } + playable := make(map[string]bool) + for k, v := range c.Menus { + for _, e := range v.MenuEntries { + tokens := strings.Split(e.Action, " ") + switch tokens[0] { + case "menu": + if _, ok := c.Menus[tokens[1]]; ok { + menus[tokens[1]] = true + } else { + return errors.New("menu action " + tokens[1] + " in menu " + k + " does not exist") + } + case "play": + if _, ok := c.Games[tokens[1]]; ok { + playable[tokens[1]] = true + } else { + return errors.New("play action " + tokens[1] + " in menu " + k + " does not exist") + } + } + } + } + // Check for unreachables + for k, _ := range c.Menus { + if _, ok := menus[k]; !ok { + return errors.New("unreachable menu : " + k) + } + } + for k, _ := range c.Games { + if _, ok := playable[k]; !ok { + return errors.New("unplayable game : " + k) + } + } + 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/pkg/config/menu_test.go b/pkg/config/menu_test.go new file mode 100644 index 0000000..e9abef1 --- /dev/null +++ b/pkg/config/menu_test.go @@ -0,0 +1,71 @@ +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 + // MenuEntries are mostly tested bellow + menu = Menu{} + if err := menu.validate("test"); err == nil { + t.Fatal("A menu without menu entries should not be valid") + } + // loop menu + menu = Menu{ + 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{ + 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/pkg/config/test_data/duplicate_game.yaml b/pkg/config/test_data/duplicate_game.yaml new file mode 100644 index 0000000..f01a017 --- /dev/null +++ b/pkg/config/test_data/duplicate_game.yaml @@ -0,0 +1,30 @@ +App: + WorkingDirectory: var/ + MaxUsers: 1 + AllowRegistration: true + MaxNickLen: 15 + MenuMaxIdleTime: 600 + +Menus: + anonymous: + MenuEntries: + - Key: q + Label: quit + Action: quit + logged_in: + MenuEntries: + - Key: p + Label: play + Action: play test + +Games: + test: + ChrootPath: test_data/fake_nethack_directory + FileMode: 0777 + Commands: + - wait + test: + ChrootPath: test_data/fake_nethack_directory + FileMode: 0777 + Commands: + - wait diff --git a/pkg/config/test_data/duplicate_menu.yaml b/pkg/config/test_data/duplicate_menu.yaml new file mode 100644 index 0000000..3dbefb7 --- /dev/null +++ b/pkg/config/test_data/duplicate_menu.yaml @@ -0,0 +1,28 @@ +App: + WorkingDirectory: var/ + MaxUsers: 1 + AllowRegistration: true + MaxNickLen: 15 + MenuMaxIdleTime: 600 + +Menus: + anonymous: + MenuEntries: + - Key: t + Label: test + Action: menu test + logged_in: + MenuEntries: + - Key: q + Label: quit + Action: quit + test: + MenuEntries: + - Key: q + Label: quit + Action: quit + test: + MenuEntries: + - Key: a + Label: login + Action: login diff --git a/pkg/config/test_data/fake_nethack_directory/.keep b/pkg/config/test_data/fake_nethack_directory/.keep new file mode 100644 index 0000000..e69de29 diff --git a/pkg/config/test_data/invalid_app.yaml b/pkg/config/test_data/invalid_app.yaml new file mode 100644 index 0000000..ed236ea --- /dev/null +++ b/pkg/config/test_data/invalid_app.yaml @@ -0,0 +1,11 @@ +Menus: + anonymous: + MenuEntries: + - Key: q + Label: quit + Action: quit + logged_in: + MenuEntries: + - Key: q + Label: quit + Action: quit diff --git a/pkg/config/test_data/invalid_game.yaml b/pkg/config/test_data/invalid_game.yaml new file mode 100644 index 0000000..d58c3ee --- /dev/null +++ b/pkg/config/test_data/invalid_game.yaml @@ -0,0 +1,21 @@ +App: + WorkingDirectory: var/ + MaxUsers: 1 + AllowRegistration: true + MaxNickLen: 15 + MenuMaxIdleTime: 600 + +Menus: + anonymous: + MenuEntries: + - Key: q + Label: quit + Action: quit + logged_in: + MenuEntries: + - Key: p + Label: play + Action: play test + +Games: + test: diff --git a/pkg/config/test_data/invalid_menus.yaml b/pkg/config/test_data/invalid_menus.yaml new file mode 100644 index 0000000..1df5fbf --- /dev/null +++ b/pkg/config/test_data/invalid_menus.yaml @@ -0,0 +1,19 @@ +App: + WorkingDirectory: var/ + MaxUsers: 1 + AllowRegistration: true + MaxNickLen: 15 + MenuMaxIdleTime: 600 + +Menus: + anonymous: + MenuEntries: + - Key: q + Label: quit + Action: quit + logged_in: + MenuEntries: + - Key: q + Label: quit + Action: quit + test: diff --git a/pkg/config/test_data/invalid_yaml b/pkg/config/test_data/invalid_yaml new file mode 100644 index 0000000..db1ddad --- /dev/null +++ b/pkg/config/test_data/invalid_yaml @@ -0,0 +1 @@ +blargh(ads) diff --git a/pkg/config/test_data/minimal.yaml b/pkg/config/test_data/minimal.yaml new file mode 100644 index 0000000..22a0a6c --- /dev/null +++ b/pkg/config/test_data/minimal.yaml @@ -0,0 +1,18 @@ +App: + WorkingDirectory: var/ + MaxUsers: 1 + AllowRegistration: true + MaxNickLen: 15 + MenuMaxIdleTime: 600 + +Menus: + anonymous: + MenuEntries: + - Key: q + Label: quit + Action: quit + logged_in: + MenuEntries: + - Key: q + Label: quit + Action: quit diff --git a/pkg/config/test_data/no_anonymous_menu.yaml b/pkg/config/test_data/no_anonymous_menu.yaml new file mode 100644 index 0000000..a015160 --- /dev/null +++ b/pkg/config/test_data/no_anonymous_menu.yaml @@ -0,0 +1,18 @@ +App: + WorkingDirectory: var/ + MaxUsers: 1 + AllowRegistration: true + MaxNickLen: 15 + MenuMaxIdleTime: 600 + +Menus: + test: + MenuEntries: + - Key: q + Label: quit + Action: quit + logged_in: + MenuEntries: + - Key: q + Label: quit + Action: quit diff --git a/pkg/config/test_data/no_logged_in_menu.yaml b/pkg/config/test_data/no_logged_in_menu.yaml new file mode 100644 index 0000000..43d0054 --- /dev/null +++ b/pkg/config/test_data/no_logged_in_menu.yaml @@ -0,0 +1,18 @@ +App: + WorkingDirectory: var/ + MaxUsers: 1 + AllowRegistration: true + MaxNickLen: 15 + MenuMaxIdleTime: 600 + +Menus: + anonymous: + MenuEntries: + - Key: q + Label: quit + Action: quit + test: + MenuEntries: + - Key: q + Label: quit + Action: quit diff --git a/pkg/config/test_data/non_existant_chopts.yaml b/pkg/config/test_data/non_existant_chopts.yaml new file mode 100644 index 0000000..d3f796d --- /dev/null +++ b/pkg/config/test_data/non_existant_chopts.yaml @@ -0,0 +1,18 @@ +App: + WorkingDirectory: var/ + MaxUsers: 1 + AllowRegistration: true + MaxNickLen: 15 + MenuMaxIdleTime: 600 + +Menus: + anonymous: + MenuEntries: + - Key: q + Label: quit + Action: quit + logged_in: + MenuEntries: + - Key: t + Label: test + Action: chopts invalid diff --git a/pkg/config/test_data/non_existant_game.yaml b/pkg/config/test_data/non_existant_game.yaml new file mode 100644 index 0000000..9bf6a38 --- /dev/null +++ b/pkg/config/test_data/non_existant_game.yaml @@ -0,0 +1,18 @@ +App: + WorkingDirectory: var/ + MaxUsers: 1 + AllowRegistration: true + MaxNickLen: 15 + MenuMaxIdleTime: 600 + +Menus: + anonymous: + MenuEntries: + - Key: q + Label: quit + Action: quit + logged_in: + MenuEntries: + - Key: t + Label: test + Action: play invalid diff --git a/pkg/config/test_data/non_existant_menu.yaml b/pkg/config/test_data/non_existant_menu.yaml new file mode 100644 index 0000000..f0f30a3 --- /dev/null +++ b/pkg/config/test_data/non_existant_menu.yaml @@ -0,0 +1,18 @@ +App: + WorkingDirectory: var/ + MaxUsers: 1 + AllowRegistration: true + MaxNickLen: 15 + MenuMaxIdleTime: 600 + +Menus: + anonymous: + MenuEntries: + - Key: t + Label: test + Action: menu invalid + logged_in: + MenuEntries: + - Key: q + Label: quit + Action: quit diff --git a/pkg/config/test_data/not_enough_menus.yaml b/pkg/config/test_data/not_enough_menus.yaml new file mode 100644 index 0000000..f585f44 --- /dev/null +++ b/pkg/config/test_data/not_enough_menus.yaml @@ -0,0 +1,13 @@ +App: + WorkingDirectory: var/ + MaxUsers: 1 + AllowRegistration: true + MaxNickLen: 15 + MenuMaxIdleTime: 600 + +Menus: + test: + MenuEntries: + - Key: q + Label: quit + Action: quit diff --git a/pkg/config/test_data/unreachable_game.yaml b/pkg/config/test_data/unreachable_game.yaml new file mode 100644 index 0000000..f2f22e2 --- /dev/null +++ b/pkg/config/test_data/unreachable_game.yaml @@ -0,0 +1,25 @@ +App: + WorkingDirectory: var/ + MaxUsers: 1 + AllowRegistration: true + MaxNickLen: 15 + MenuMaxIdleTime: 600 + +Menus: + anonymous: + MenuEntries: + - Key: q + Label: quit + Action: quit + logged_in: + MenuEntries: + - Key: q + Label: quit + Action: quit + +Games: + unreachable: + ChrootPath: test_data/fake_nethack_directory + FileMode: 0777 + Commands: + - wait diff --git a/pkg/config/test_data/unreachable_menu.yaml b/pkg/config/test_data/unreachable_menu.yaml new file mode 100644 index 0000000..f947cf7 --- /dev/null +++ b/pkg/config/test_data/unreachable_menu.yaml @@ -0,0 +1,23 @@ +App: + WorkingDirectory: var/ + MaxUsers: 1 + AllowRegistration: true + MaxNickLen: 15 + MenuMaxIdleTime: 600 + +Menus: + anonymous: + MenuEntries: + - Key: q + Label: quit + Action: quit + logged_in: + MenuEntries: + - Key: q + Label: quit + Action: quit + test: + MenuEntries: + - Key: q + Label: quit + Action: quit -- cgit v1.2.3