diff --git a/cmd/rove-server/main.go b/cmd/rove-server/main.go index b243814..22efc1f 100644 --- a/cmd/rove-server/main.go +++ b/cmd/rove-server/main.go @@ -3,28 +3,21 @@ package main import ( "flag" "fmt" - "log" - "net/http" "os" "os/signal" "syscall" - "github.com/mdiluz/rove/pkg/rovegame" + "github.com/mdiluz/rove/pkg/server" ) var port = flag.Int("port", 8080, "The port to host on") func main() { + server := server.NewServer(*port) fmt.Println("Initialising...") - // Set up the world - world := rovegame.NewWorld() - fmt.Printf("World created\n\t%+v\n", world) - - // Create a new router - router := NewRouter() - fmt.Printf("Router Created\n") + server.Initialise() // Set up the close handler c := make(chan os.Signal) @@ -37,9 +30,5 @@ func main() { fmt.Println("Initialised") - // Listen and serve the http requests - fmt.Println("Serving HTTP") - if err := http.ListenAndServe(fmt.Sprintf(":%d", *port), router); err != nil { - log.Fatal(err) - } + server.Run() } diff --git a/cmd/rove-server/player.go b/cmd/rove-server/player.go deleted file mode 100644 index 42e2f3a..0000000 --- a/cmd/rove-server/player.go +++ /dev/null @@ -1,13 +0,0 @@ -package main - -import "github.com/google/uuid" - -type Player struct { - id uuid.UUID -} - -func NewPlayer() Player { - return Player{ - id: uuid.New(), - } -} diff --git a/cmd/rove-server/player_test.go b/cmd/rove-server/player_test.go deleted file mode 100644 index b26d265..0000000 --- a/cmd/rove-server/player_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package main - -import ( - "testing" -) - -func TestNewPlayer(t *testing.T) { - a := NewPlayer() - b := NewPlayer() - if a.id == b.id { - t.Error("Player IDs matched") - } -} diff --git a/cmd/rove-server/router.go b/cmd/rove-server/router.go deleted file mode 100644 index 59d24f9..0000000 --- a/cmd/rove-server/router.go +++ /dev/null @@ -1,56 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "net/http" - - "github.com/gorilla/mux" - "github.com/mdiluz/rove/pkg/rove" -) - -// NewRouter sets up the server mux -func NewRouter() (router *mux.Router) { - router = mux.NewRouter().StrictSlash(true) - - // Set up the handlers - router.HandleFunc("/status", HandleStatus) - router.HandleFunc("/register", HandleRegister) - - return -} - -// HandleStatus handles HTTP requests to the /status endpoint -func HandleStatus(w http.ResponseWriter, r *http.Request) { - fmt.Printf("%s\t%s", r.Method, r.RequestURI) - - var response = rove.StatusResponse{ - Ready: true, - } - - // Be a good citizen and set the header for the return - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusOK) - - // Reply with the current status - json.NewEncoder(w).Encode(response) -} - -// HandleRegister handles HTTP requests to the /register endpoint -func HandleRegister(w http.ResponseWriter, r *http.Request) { - fmt.Printf("%s\t%s", r.Method, r.RequestURI) - - // TODO: Add this user to the server - player := NewPlayer() - var response = rove.RegisterResponse{ - Success: true, - Id: player.id.String(), - } - - // Be a good citizen and set the header for the return - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusOK) - - // Reply with the current status - json.NewEncoder(w).Encode(response) -} diff --git a/pkg/rovegame/world.go b/pkg/game/world.go similarity index 97% rename from pkg/rovegame/world.go rename to pkg/game/world.go index 1e1a4e3..f03d99c 100644 --- a/pkg/rovegame/world.go +++ b/pkg/game/world.go @@ -1,4 +1,4 @@ -package rovegame +package game import "github.com/google/uuid" diff --git a/pkg/rovegame/world_test.go b/pkg/game/world_test.go similarity index 95% rename from pkg/rovegame/world_test.go rename to pkg/game/world_test.go index c8124ad..504b8ee 100644 --- a/pkg/rovegame/world_test.go +++ b/pkg/game/world_test.go @@ -1,4 +1,4 @@ -package rovegame +package game import ( "testing" diff --git a/pkg/rove/rove.go b/pkg/rove/rove.go index 2c91901..cdfed09 100644 --- a/pkg/rove/rove.go +++ b/pkg/rove/rove.go @@ -1,10 +1,13 @@ package rove import ( + "bytes" "encoding/json" "fmt" "net/http" "net/url" + + "github.com/mdiluz/rove/pkg/server" ) // Connection is the container for a simple connection to the server @@ -19,13 +22,8 @@ func NewConnection(host string) *Connection { } } -// StatusResponse is a struct that contains information on the status of the server -type StatusResponse struct { - Ready bool `json:"ready"` -} - // Status returns the current status of the server -func (c *Connection) Status() (status StatusResponse, err error) { +func (c *Connection) Status() (status server.StatusResponse, err error) { url := url.URL{ Scheme: "http", Host: c.host, @@ -33,9 +31,9 @@ func (c *Connection) Status() (status StatusResponse, err error) { } if resp, err := http.Get(url.String()); err != nil { - return StatusResponse{}, err + return server.StatusResponse{}, err } else if resp.StatusCode != http.StatusOK { - return StatusResponse{}, fmt.Errorf("Status request returned %d", resp.StatusCode) + return server.StatusResponse{}, fmt.Errorf("Status request returned %d", resp.StatusCode) } else { err = json.NewDecoder(resp.Body).Decode(&status) } @@ -43,26 +41,36 @@ func (c *Connection) Status() (status StatusResponse, err error) { return } -// RegisterResponse -type RegisterResponse struct { - Id string `json:"id"` - Success bool `json:"success"` -} - -// Register registers a new player on the server -func (c *Connection) Register() (register RegisterResponse, err error) { +// Register registers a new account on the server +func (c *Connection) Register(name string) (register server.RegisterResponse, err error) { url := url.URL{ Scheme: "http", Host: c.host, Path: "register", } - if resp, err := http.Get(url.String()); err != nil { - return RegisterResponse{}, err - } else if resp.StatusCode != http.StatusOK { - return RegisterResponse{}, fmt.Errorf("Status request returned %d", resp.StatusCode) + // Marshal the register data struct + data := server.RegisterData{Name: name} + marshalled, err := json.Marshal(data) + + // Set up the request + req, err := http.NewRequest("POST", url.String(), bytes.NewReader(marshalled)) + req.Header.Set("Content-Type", "application/json") + + // Do the request + client := &http.Client{} + if resp, err := client.Do(req); err != nil { + return server.RegisterResponse{}, err } else { - err = json.NewDecoder(resp.Body).Decode(®ister) + defer resp.Body.Close() + + // Handle any errors + if resp.StatusCode != http.StatusOK { + return server.RegisterResponse{}, fmt.Errorf("Status request returned %d", resp.StatusCode) + } else { + // Decode the reply + err = json.NewDecoder(resp.Body).Decode(®ister) + } } return diff --git a/pkg/rove/rove_test.go b/pkg/rove/rove_test.go index 2a8e4ce..a78758d 100644 --- a/pkg/rove/rove_test.go +++ b/pkg/rove/rove_test.go @@ -10,6 +10,7 @@ var serverUrl = "localhost:8080" func TestStatus(t *testing.T) { conn := NewConnection(serverUrl) + if status, err := conn.Status(); err != nil { t.Errorf("Status returned error: %s", err) } else if !status.Ready { @@ -19,11 +20,28 @@ func TestStatus(t *testing.T) { func TestRegister(t *testing.T) { conn := NewConnection(serverUrl) - if reg, err := conn.Register(); err != nil { + + reg1, err := conn.Register("one") + if err != nil { t.Errorf("Register returned error: %s", err) - } else if !reg.Success { + } else if !reg1.Success { t.Error("Server did not success for Register") - } else if len(reg.Id) == 0 { + } else if len(reg1.Id) == 0 { t.Error("Server returned empty registration ID") } + + reg2, err := conn.Register("two") + if err != nil { + t.Errorf("Register returned error: %s", err) + } else if !reg2.Success { + t.Error("Server did not success for Register") + } else if len(reg2.Id) == 0 { + t.Error("Server returned empty registration ID") + } + + if reg2, err := conn.Register("one"); err != nil { + t.Errorf("Register returned error: %s", err) + } else if reg2.Success { + t.Error("Server should have failed to register duplicate name") + } } diff --git a/pkg/server/accounts.go b/pkg/server/accounts.go new file mode 100644 index 0000000..2f68051 --- /dev/null +++ b/pkg/server/accounts.go @@ -0,0 +1,47 @@ +package server + +import ( + "fmt" + + "github.com/google/uuid" +) + +// Account represents a registered user +type Account struct { + // Name simply describes the account and must be unique + Name string + + // id represents a unique ID per account and is set one registered + id uuid.UUID +} + +// Accountant manages a set of accounts +type Accountant struct { + accounts []Account +} + +// NewAccountant creates a new accountant +func NewAccountant() *Accountant { + return &Accountant{} +} + +// RegisterAccount adds an account to the set of internal accounts +func (a *Accountant) RegisterAccount(acc Account) (Account, error) { + + // Set the account ID to a new UUID + acc.id = uuid.New() + + // Verify this acount isn't already registered + for _, a := range a.accounts { + if a.Name == acc.Name { + return Account{}, fmt.Errorf("Account name already registered") + } else if a.id == acc.id { + return Account{}, fmt.Errorf("Account ID already registered") + } + } + + // Simply add the account to the list + a.accounts = append(a.accounts, acc) + + return acc, nil +} diff --git a/pkg/server/accounts_test.go b/pkg/server/accounts_test.go new file mode 100644 index 0000000..8398ba0 --- /dev/null +++ b/pkg/server/accounts_test.go @@ -0,0 +1,49 @@ +package server + +import ( + "testing" +) + +func TestNewAccountant(t *testing.T) { + // Very basic verify here for now + accountant := NewAccountant() + if accountant == nil { + t.Error("Failed to create accountant") + } +} + +func TestAccountant_RegisterAccount(t *testing.T) { + + accountant := NewAccountant() + + // Start by making two accounts + + namea := "one" + a := Account{Name: namea} + acca, err := accountant.RegisterAccount(a) + if err != nil { + t.Error(err) + } else if acca.Name != namea { + t.Errorf("Missmatched account name after register, expected: %s, actual: %s", namea, acca.Name) + } + + nameb := "two" + b := Account{Name: nameb} + accb, err := accountant.RegisterAccount(b) + if err != nil { + t.Error(err) + } else if accb.Name != nameb { + t.Errorf("Missmatched account name after register, expected: %s, actual: %s", nameb, acca.Name) + } + + // Verify our accounts have differing IDs + if acca.id == accb.id { + t.Error("Duplicate account IDs fo separate accounts") + } + + // Verify another request gets rejected + _, err = accountant.RegisterAccount(a) + if err == nil { + t.Error("Duplicate account name did not produce error") + } +} diff --git a/pkg/server/router.go b/pkg/server/router.go new file mode 100644 index 0000000..5246dd2 --- /dev/null +++ b/pkg/server/router.go @@ -0,0 +1,97 @@ +package server + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/gorilla/mux" +) + +// NewRouter sets up the server mux +func (s *Server) SetUpRouter() { + s.router = mux.NewRouter().StrictSlash(true) + + // Set up the handlers + s.router.HandleFunc("/status", s.HandleStatus) + s.router.HandleFunc("/register", s.HandleRegister) +} + +// StatusResponse is a struct that contains information on the status of the server +type StatusResponse struct { + Ready bool `json:"ready"` +} + +// HandleStatus handles HTTP requests to the /status endpoint +func (s *Server) HandleStatus(w http.ResponseWriter, r *http.Request) { + fmt.Printf("%s\t%s", r.Method, r.RequestURI) + + // Verify we're hit with a get request + if r.Method != http.MethodGet { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + var response = StatusResponse{ + Ready: true, + } + + // Be a good citizen and set the header for the return + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + w.WriteHeader(http.StatusOK) + + // Reply with the current status + json.NewEncoder(w).Encode(response) +} + +// RegisterData describes the data to send when registering +type RegisterData struct { + Name string `json:"id"` +} + +// RegisterResponse describes the response to a register request +type RegisterResponse struct { + Id string `json:"id"` + + Success bool `json:"success"` + Error string `json:"error"` +} + +// HandleRegister handles HTTP requests to the /register endpoint +func (s *Server) HandleRegister(w http.ResponseWriter, r *http.Request) { + fmt.Printf("%s\t%s", r.Method, r.RequestURI) + + // Verify we're hit with a get request + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + // Pull out the registration info + var data RegisterData + json.NewDecoder(r.Body).Decode(&data) + + // Register the account with the server + acc := Account{Name: data.Name} + acc, err := s.accountant.RegisterAccount(acc) + + // Set up the response + var response = RegisterResponse{ + Success: false, + } + + // If we didn't fail, respond with the account ID string + if err == nil { + response.Success = true + response.Id = acc.id.String() + } else { + response.Error = err.Error() + } + + // Be a good citizen and set the header for the return + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + w.WriteHeader(http.StatusOK) + + // Reply with the current status + json.NewEncoder(w).Encode(response) +} diff --git a/cmd/rove-server/router_test.go b/pkg/server/router_test.go similarity index 74% rename from cmd/rove-server/router_test.go rename to pkg/server/router_test.go index de7ae5b..8bbd892 100644 --- a/cmd/rove-server/router_test.go +++ b/pkg/server/router_test.go @@ -1,21 +1,22 @@ -package main +package server import ( "encoding/json" "net/http" "net/http/httptest" "testing" - - "github.com/mdiluz/rove/pkg/rove" ) func TestHandleStatus(t *testing.T) { request, _ := http.NewRequest(http.MethodGet, "/status", nil) response := httptest.NewRecorder() - HandleStatus(response, request) + s := NewServer(8080) + s.Initialise() - var status rove.StatusResponse + s.HandleStatus(response, request) + + var status StatusResponse json.NewDecoder(response.Body).Decode(&status) if status.Ready != true { diff --git a/pkg/server/server.go b/pkg/server/server.go new file mode 100644 index 0000000..f1024e4 --- /dev/null +++ b/pkg/server/server.go @@ -0,0 +1,49 @@ +package server + +import ( + "fmt" + "log" + "net/http" + + "github.com/gorilla/mux" + "github.com/mdiluz/rove/pkg/game" +) + +// Server contains the relevant data to run a game server +type Server struct { + port int + + accountant *Accountant + world *game.World + + router *mux.Router +} + +// NewServer sets up a new server +func NewServer(port int) *Server { + return &Server{ + port: port, + accountant: NewAccountant(), + world: game.NewWorld(), + } +} + +// Initialise sets up internal state ready to serve +func (s *Server) Initialise() { + // Set up the world + s.world = game.NewWorld() + fmt.Printf("World created\n\t%+v\n", s.world) + + // Create a new router + s.SetUpRouter() + fmt.Printf("Routes Created\n") +} + +// Run executes the server +func (s *Server) Run() { + // Listen and serve the http requests + fmt.Println("Serving HTTP") + if err := http.ListenAndServe(fmt.Sprintf(":%d", s.port), s.router); err != nil { + log.Fatal(err) + } +}