diff --git a/.travis.yml b/.travis.yml index a7db419..1c03fcf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,11 @@ language: go go: - - 1.3.1 \ No newline at end of file + - 1.3.1 + +before_script: + - source /etc/lsb-release && echo "deb http://download.rethinkdb.com/apt $DISTRIB_CODENAME main" | sudo tee /etc/apt/sources.list.d/rethinkdb.list + - wget -qO- http://download.rethinkdb.com/apt/pubkey.gpg | sudo apt-key add - + - sudo apt-get update + - sudo apt-get install rethinkdb + - rethinkdb --bind all & diff --git a/db/setup.go b/db/setup.go index 835a146..e3fd1f6 100644 --- a/db/setup.go +++ b/db/setup.go @@ -27,6 +27,7 @@ var databaseNames = []string{ "prod", "staging", "dev", + "test", } // Setup configures the RethinkDB server diff --git a/env/config.go b/env/config.go index 40d8551..880cb12 100644 --- a/env/config.go +++ b/env/config.go @@ -2,9 +2,15 @@ package env // Flags contains values of flags which are important in the whole API type Flags struct { - BindAddress string - APIVersion string - LogFormatterType string + BindAddress string + APIVersion string + LogFormatterType string + ForceColors bool + SessionDuration int ClassicRegistration bool + + RethinkDBURL string + RethinkDBKey string + RethinkDBDatabase string } diff --git a/main.go b/main.go index 1ef2346..8d20261 100644 --- a/main.go +++ b/main.go @@ -4,19 +4,14 @@ import ( "net" "net/http" "os" - "time" "github.com/Sirupsen/logrus" - "github.com/dancannon/gorethink" - "github.com/lavab/glogrus" + "github.com/namsral/flag" "github.com/zenazn/goji/graceful" - "github.com/zenazn/goji/web" - "github.com/zenazn/goji/web/middleware" - "github.com/lavab/api/db" "github.com/lavab/api/env" - "github.com/lavab/api/routes" + "github.com/lavab/api/setup" ) // TODO: "Middleware that implements a few quick security wins" @@ -35,7 +30,7 @@ var ( rethinkdbURL = flag.String("rethinkdb_url", func() string { address := os.Getenv("RETHINKDB_PORT_28015_TCP_ADDR") if address == "" { - address = "localhost" + address = "127.0.0.1" } return address + ":28015" }(), "Address of the RethinkDB database") @@ -55,163 +50,28 @@ func main() { // Put config into the environment package env.Config = &env.Flags{ - BindAddress: *bindAddress, - APIVersion: *apiVersion, - LogFormatterType: *logFormatterType, - SessionDuration: *sessionDuration, - ClassicRegistration: *classicRegistration, - } - - // Set up a new logger - log := logrus.New() + BindAddress: *bindAddress, + APIVersion: *apiVersion, + LogFormatterType: *logFormatterType, + ForceColors: *forceColors, - // Set the formatter depending on the passed flag's value - if *logFormatterType == "text" { - log.Formatter = &logrus.TextFormatter{ - ForceColors: *forceColors, - } - } else if *logFormatterType == "json" { - log.Formatter = &logrus.JSONFormatter{} - } - - // Pass it to the environment package - env.Log = log - - // Set up the database - rethinkOpts := gorethink.ConnectOpts{ - Address: *rethinkdbURL, - AuthKey: *rethinkdbKey, - MaxIdle: 10, - IdleTimeout: time.Second * 10, - } - err := db.Setup(rethinkOpts) - if err != nil { - log.WithFields(logrus.Fields{ - "error": err, - }).Fatal("Unable to set up the database") - } - - // Initialize the actual connection - rethinkOpts.Database = *rethinkdbDatabase - rethinkSession, err := gorethink.Connect(rethinkOpts) - if err != nil { - log.WithFields(logrus.Fields{ - "error": err, - }).Fatal("Unable to connect to the database") - } - - // Put the RethinkDB session into the environment package - env.Rethink = rethinkSession + ClassicRegistration: *classicRegistration, + SessionDuration: *sessionDuration, - // Initialize the tables - env.Tokens = &db.TokensTable{ - RethinkCRUD: db.NewCRUDTable( - rethinkSession, - rethinkOpts.Database, - "tokens", - ), - } - env.Accounts = &db.AccountsTable{ - RethinkCRUD: db.NewCRUDTable( - rethinkSession, - rethinkOpts.Database, - "accounts", - ), - Tokens: env.Tokens, - } - env.Keys = &db.KeysTable{ - RethinkCRUD: db.NewCRUDTable( - rethinkSession, - rethinkOpts.Database, - "keys", - ), - } - env.Contacts = &db.ContactsTable{ - RethinkCRUD: db.NewCRUDTable( - rethinkSession, - rethinkOpts.Database, - "contacts", - ), + RethinkDBURL: *rethinkdbURL, + RethinkDBKey: *rethinkdbKey, + RethinkDBDatabase: *rethinkdbDatabase, } - // Create a new goji mux - mux := web.New() - - // Include the most basic middlewares: - // - RequestID assigns an unique ID for each request in order to identify errors. - // - Glogrus logs each request - // - Recoverer prevents panics from crashing the API - // - AutomaticOptions automatically responds to OPTIONS requests - mux.Use(middleware.RequestID) - mux.Use(glogrus.NewGlogrus(log, "api")) - mux.Use(middleware.Recoverer) - mux.Use(middleware.AutomaticOptions) - - // Set up an auth'd mux - auth := web.New() - auth.Use(routes.AuthMiddleware) - - // Index route - mux.Get("/", routes.Hello) - - // Accounts - auth.Get("/accounts", routes.AccountsList) - mux.Post("/accounts", routes.AccountsCreate) - auth.Get("/accounts/:id", routes.AccountsGet) - auth.Put("/accounts/:id", routes.AccountsUpdate) - auth.Delete("/accounts/:id", routes.AccountsDelete) - auth.Post("/accounts/:id/wipe-data", routes.AccountsWipeData) - - // Tokens - auth.Get("/tokens", routes.TokensGet) - mux.Post("/tokens", routes.TokensCreate) - auth.Delete("/tokens", routes.TokensDelete) - auth.Delete("/tokens/:id", routes.TokensDelete) - - // Threads - auth.Get("/threads", routes.ThreadsList) - auth.Get("/threads/:id", routes.ThreadsGet) - auth.Put("/threads/:id", routes.ThreadsUpdate) - - // Emails - auth.Get("/emails", routes.EmailsList) - auth.Post("/emails", routes.EmailsCreate) - auth.Get("/emails/:id", routes.EmailsGet) - auth.Put("/emails/:id", routes.EmailsUpdate) - auth.Delete("/emails/:id", routes.EmailsDelete) - - // Labels - auth.Get("/labels", routes.LabelsList) - auth.Post("/labels", routes.LabelsCreate) - auth.Get("/labels/:id", routes.LabelsGet) - auth.Put("/labels/:id", routes.LabelsUpdate) - auth.Delete("/labels/:id", routes.LabelsDelete) - - // Contacts - auth.Get("/contacts", routes.ContactsList) - auth.Post("/contacts", routes.ContactsCreate) - auth.Get("/contacts/:id", routes.ContactsGet) - auth.Put("/contacts/:id", routes.ContactsUpdate) - auth.Delete("/contacts/:id", routes.ContactsDelete) - - // Keys - mux.Get("/keys", routes.KeysList) - auth.Post("/keys", routes.KeysCreate) - mux.Get("/keys/:id", routes.KeysGet) - auth.Post("/keys/:id/vote", routes.KeysVote) - - // Merge the muxes - mux.Handle("/*", auth) - - // Compile the routes - mux.Compile() + // Generate a mux + mux := setup.PrepareMux(env.Config) // Make the mux handle every request http.Handle("/", mux) // Log that we're starting the server - log.WithFields(logrus.Fields{ - "address": *bindAddress, + env.Log.WithFields(logrus.Fields{ + "address": env.Config.BindAddress, }).Info("Starting the HTTP server") // Initialize the goroutine listening to signals passed to the app @@ -219,18 +79,18 @@ func main() { // Pre-graceful shutdown event graceful.PreHook(func() { - log.Info("Received a singnal, stopping the application") + env.Log.Info("Received a singnal, stopping the application") }) // Post-shutdown event graceful.PostHook(func() { - log.Info("Stopped the application") + env.Log.Info("Stopped the application") }) // Listen to the passed address - listener, err := net.Listen("tcp", *bindAddress) + listener, err := net.Listen("tcp", env.Config.BindAddress) if err != nil { - log.WithFields(logrus.Fields{ + env.Log.WithFields(logrus.Fields{ "error": err, "address": *bindAddress, }).Fatal("Cannot set up a TCP listener") @@ -240,7 +100,7 @@ func main() { err = graceful.Serve(listener, http.DefaultServeMux) if err != nil { // Don't use .Fatal! We need the code to shut down properly. - log.Error(err) + env.Log.Error(err) } // If code reaches this place, it means that it was forcefully closed. diff --git a/routes/accounts.go b/routes/accounts.go index eaf9618..cb775d7 100644 --- a/routes/accounts.go +++ b/routes/accounts.go @@ -21,16 +21,16 @@ type AccountsListResponse struct { func AccountsList(w http.ResponseWriter, r *http.Request) { utils.JSONResponse(w, 501, &AccountsListResponse{ Success: false, - Message: "Method not implemented", + Message: "Sorry, not implemented yet", }) } // AccountsCreateRequest contains the input for the AccountsCreate endpoint. type AccountsCreateRequest struct { - Token string `json:"token" schema:"token"` - Username string `json:"username" schema:"username"` - Password string `json:"password" schema:"password"` - AltEmail string `json:"alt_email" schema:"alt_email"` + Token string `json:"token,omitempty" schema:"token"` + Username string `json:"username,omitempty" schema:"username"` + Password string `json:"password,omitempty" schema:"password"` + AltEmail string `json:"alt_email,omitempty" schema:"alt_email"` } // AccountsCreateResponse contains the output of the AccountsCreate request. @@ -64,14 +64,14 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) { requestType := "unknown" if input.AltEmail == "" && input.Username != "" && input.Password != "" && input.Token != "" { requestType = "invited" - } else if input.AltEmail != "" && input.Username != "" && input.Password != "" && input.Token != "" { + } else if input.AltEmail != "" && input.Username != "" && input.Password != "" && input.Token == "" { requestType = "classic" } else if input.AltEmail != "" && input.Username == "" && input.Password == "" && input.Token == "" { requestType = "queue" } // "unknown" requests are empty and invalid - if requestType == "invalid" { + if requestType == "unknown" { utils.JSONResponse(w, 400, &AccountsCreateResponse{ Success: false, Message: "Invalid request", @@ -222,20 +222,13 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) { type AccountsGetResponse struct { Success bool `json:"success"` Message string `json:"message,omitempty"` - User *models.Account `json:"user,omitempty"` + Account *models.Account `json:"user,omitempty"` } // AccountsGet returns the information about the specified account func AccountsGet(c web.C, w http.ResponseWriter, r *http.Request) { // Get the account ID from the request - id, ok := c.URLParams["id"] - if !ok { - utils.JSONResponse(w, 409, &AccountsGetResponse{ - Success: false, - Message: "Invalid user ID", - }) - return - } + id := c.URLParams["id"] // Right now we only support "me" as the ID if id != "me" { @@ -252,27 +245,9 @@ func AccountsGet(c web.C, w http.ResponseWriter, r *http.Request) { // Fetch the user object from the database user, err := env.Accounts.GetAccount(session.Owner) if err != nil { - // The session refers to a non-existing user - env.Log.WithFields(logrus.Fields{ - "id": session.ID, - "error": err, - }).Warn("Valid session referred to a removed account") - - // Try to remove the orphaned session - if err := env.Tokens.DeleteID(session.ID); err != nil { - env.Log.WithFields(logrus.Fields{ - "id": session.ID, - "error": err, - }).Error("Unable to remove an orphaned session") - } else { - env.Log.WithFields(logrus.Fields{ - "id": session.ID, - }).Info("Removed an orphaned session") - } - - utils.JSONResponse(w, 410, &AccountsGetResponse{ + utils.JSONResponse(w, 500, &AccountsDeleteResponse{ Success: false, - Message: "Account disabled", + Message: "Unable to resolve the account", }) return } @@ -280,13 +255,12 @@ func AccountsGet(c web.C, w http.ResponseWriter, r *http.Request) { // Return the user struct utils.JSONResponse(w, 200, &AccountsGetResponse{ Success: true, - User: user, + Account: user, }) } // AccountsUpdateRequest contains the input for the AccountsUpdate endpoint. type AccountsUpdateRequest struct { - Type string `json:"type" schema:"type"` AltEmail string `json:"alt_email" schema:"alt_email"` CurrentPassword string `json:"current_password" schema:"current_password"` NewPassword string `json:"new_password" schema:"new_password"` @@ -317,14 +291,7 @@ func AccountsUpdate(c web.C, w http.ResponseWriter, r *http.Request) { } // Get the account ID from the request - id, ok := c.URLParams["id"] - if !ok { - utils.JSONResponse(w, 409, &AccountsUpdateResponse{ - Success: false, - Message: "Invalid user ID", - }) - return - } + id := c.URLParams["id"] // Right now we only support "me" as the ID if id != "me" { @@ -341,27 +308,9 @@ func AccountsUpdate(c web.C, w http.ResponseWriter, r *http.Request) { // Fetch the user object from the database user, err := env.Accounts.GetAccount(session.Owner) if err != nil { - // The session refers to a non-existing user - env.Log.WithFields(logrus.Fields{ - "id": session.ID, - "error": err, - }).Warn("Valid session referred to a removed account") - - // Try to remove the orphaned session - if err := env.Tokens.DeleteID(session.ID); err != nil { - env.Log.WithFields(logrus.Fields{ - "id": session.ID, - "error": err, - }).Error("Unable to remove an orphaned session") - } else { - env.Log.WithFields(logrus.Fields{ - "id": session.ID, - }).Info("Removed an orphaned session") - } - - utils.JSONResponse(w, 410, &AccountsUpdateResponse{ + utils.JSONResponse(w, 500, &AccountsDeleteResponse{ Success: false, - Message: "Account disabled", + Message: "Unable to resolve the account", }) return } @@ -374,17 +323,19 @@ func AccountsUpdate(c web.C, w http.ResponseWriter, r *http.Request) { return } - err = user.SetPassword(input.NewPassword) - if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err, - }).Error("Unable to hash a password") + if input.NewPassword != "" { + err = user.SetPassword(input.NewPassword) + if err != nil { + env.Log.WithFields(logrus.Fields{ + "error": err, + }).Error("Unable to hash a password") - utils.JSONResponse(w, 500, &AccountsUpdateResponse{ - Success: false, - Message: "Internal error (code AC/UP/01)", - }) - return + utils.JSONResponse(w, 500, &AccountsUpdateResponse{ + Success: false, + Message: "Internal error (code AC/UP/01)", + }) + return + } } if input.AltEmail != "" { @@ -405,7 +356,7 @@ func AccountsUpdate(c web.C, w http.ResponseWriter, r *http.Request) { } utils.JSONResponse(w, 200, &AccountsUpdateResponse{ - Success: false, + Success: true, Message: "Your account has been successfully updated", Account: user, }) @@ -420,14 +371,7 @@ type AccountsDeleteResponse struct { // AccountsDelete deletes an account and everything related to it. func AccountsDelete(c web.C, w http.ResponseWriter, r *http.Request) { // Get the account ID from the request - id, ok := c.URLParams["id"] - if !ok { - utils.JSONResponse(w, 409, &AccountsDeleteResponse{ - Success: false, - Message: "Invalid user ID", - }) - return - } + id := c.URLParams["id"] // Right now we only support "me" as the ID if id != "me" { @@ -444,27 +388,9 @@ func AccountsDelete(c web.C, w http.ResponseWriter, r *http.Request) { // Fetch the user object from the database user, err := env.Accounts.GetAccount(session.Owner) if err != nil { - // The session refers to a non-existing user - env.Log.WithFields(logrus.Fields{ - "id": session.ID, - "error": err, - }).Warn("Valid session referred to a removed account") - - // Try to remove the orphaned session - if err := env.Tokens.DeleteID(session.ID); err != nil { - env.Log.WithFields(logrus.Fields{ - "id": session.ID, - "error": err, - }).Error("Unable to remove an orphaned session") - } else { - env.Log.WithFields(logrus.Fields{ - "id": session.ID, - }).Info("Removed an orphaned session") - } - - utils.JSONResponse(w, 410, &AccountsDeleteResponse{ + utils.JSONResponse(w, 500, &AccountsDeleteResponse{ Success: false, - Message: "Account disabled", + Message: "Unable to resolve the account", }) return } @@ -521,14 +447,7 @@ type AccountsWipeDataResponse struct { // AccountsWipeData wipes all data except the actual account and billing info. func AccountsWipeData(c web.C, w http.ResponseWriter, r *http.Request) { // Get the account ID from the request - id, ok := c.URLParams["id"] - if !ok { - utils.JSONResponse(w, 409, &AccountsWipeDataResponse{ - Success: false, - Message: "Invalid user ID", - }) - return - } + id := c.URLParams["id"] // Right now we only support "me" as the ID if id != "me" { diff --git a/routes/accounts_test.go b/routes/accounts_test.go new file mode 100644 index 0000000..6c07c8f --- /dev/null +++ b/routes/accounts_test.go @@ -0,0 +1,608 @@ +package routes_test + +import ( + "testing" + "time" + + "github.com/franela/goreq" + "github.com/stretchr/testify/require" + + "github.com/lavab/api/env" + "github.com/lavab/api/models" + "github.com/lavab/api/routes" +) + +func TestAccountsCreateInvalid(t *testing.T) { + result, err := goreq.Request{ + Method: "POST", + Uri: server.URL + "/accounts", + ContentType: "application/json", + Body: "!@#!@#", + }.Do() + require.Nil(t, err) + + var response routes.AccountsCreateResponse + err = result.Body.FromJsonTo(&response) + env.Log.Print(response) + require.Nil(t, err) + require.False(t, response.Success) + require.Equal(t, "Invalid input format", response.Message) +} + +func TestAccountsCreateUnknown(t *testing.T) { + // POST /accounts - unknown + result, err := goreq.Request{ + Method: "POST", + Uri: server.URL + "/accounts", + }.Do() + require.Nil(t, err) + + // Unmarshal the response + var response routes.AccountsCreateResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + // Check values + require.False(t, response.Success) + require.Equal(t, "Invalid request", response.Message) +} + +func TestAccountsCreateInvited(t *testing.T) { + const ( + username = "jeremy" + password = "potato" + ) + + // Prepare a token + inviteToken := models.Token{ + Resource: models.MakeResource("", "test invite token"), + Type: "invite", + } + inviteToken.ExpireSoon() + + err := env.Tokens.Insert(inviteToken) + require.Nil(t, err) + + // POST /accounts - invited + result1, err := goreq.Request{ + Method: "POST", + Uri: server.URL + "/accounts", + ContentType: "application/json", + Body: routes.AccountsCreateRequest{ + Username: username, + Password: password, + Token: inviteToken.ID, + }, + }.Do() + require.Nil(t, err) + + // Unmarshal the response + var response1 routes.AccountsCreateResponse + err = result1.Body.FromJsonTo(&response1) + require.Nil(t, err) + + // Check the result's contents + require.True(t, response1.Success) + require.Equal(t, "A new account was successfully created", response1.Message) + require.NotEmpty(t, response1.Account.ID) + + accountID = response1.Account.ID + + // POST /accounts - invited with wrong token + result2, err := goreq.Request{ + Method: "POST", + Uri: server.URL + "/accounts", + ContentType: "application/json", + Body: routes.AccountsCreateRequest{ + Username: username + "2", + Password: password, + Token: "asdasdasd", + }, + }.Do() + require.Nil(t, err) + + // Unmarshal the response + var response2 routes.AccountsCreateResponse + err = result2.Body.FromJsonTo(&response2) + require.Nil(t, err) + + // Check the result's contents + require.False(t, response2.Success) + require.Equal(t, "Invalid invitation token", response2.Message) +} + +func TestAccountsCreateInvitedExisting(t *testing.T) { + const ( + username = "jeremy" + password = "potato" + ) + + // Prepare a token + inviteToken := models.Token{ + Resource: models.MakeResource("", "test invite token"), + Type: "invite", + } + inviteToken.ExpireSoon() + + err := env.Tokens.Insert(inviteToken) + require.Nil(t, err) + + // POST /accounts - invited + result, err := goreq.Request{ + Method: "POST", + Uri: server.URL + "/accounts", + ContentType: "application/json", + Body: routes.AccountsCreateRequest{ + Username: username, + Password: password, + Token: inviteToken.ID, + }, + }.Do() + require.Nil(t, err) + + // Unmarshal the response + var response routes.AccountsCreateResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + // Check the result's contents + require.False(t, response.Success) + require.Equal(t, "Username already exists", response.Message) +} + +func TestAccountsCreateInvitedExpired(t *testing.T) { + const ( + username = "jeremy2" + password = "potato2" + ) + + // Prepare a token + inviteToken := models.Token{ + Resource: models.MakeResource("", "test invite token"), + Type: "invite", + } + inviteToken.ExpiryDate = time.Now().Truncate(time.Hour) + + err := env.Tokens.Insert(inviteToken) + require.Nil(t, err) + + // POST /accounts - invited + result, err := goreq.Request{ + Method: "POST", + Uri: server.URL + "/accounts", + ContentType: "application/json", + Body: routes.AccountsCreateRequest{ + Username: username, + Password: password, + Token: inviteToken.ID, + }, + }.Do() + require.Nil(t, err) + + // Unmarshal the response + var response routes.AccountsCreateResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + // Check the result's contents + require.False(t, response.Success) + require.Equal(t, "Expired invitation token", response.Message) +} + +func TestAccountsCreateInvitedWrongType(t *testing.T) { + const ( + username = "jeremy2" + password = "potato2" + ) + + // Prepare a token + inviteToken := models.Token{ + Resource: models.MakeResource("", "test not invite token"), + Type: "not invite", + } + inviteToken.ExpiryDate = time.Now().Truncate(time.Hour) + + err := env.Tokens.Insert(inviteToken) + require.Nil(t, err) + + // POST /accounts - invited + result, err := goreq.Request{ + Method: "POST", + Uri: server.URL + "/accounts", + ContentType: "application/json", + Body: routes.AccountsCreateRequest{ + Username: username, + Password: password, + Token: inviteToken.ID, + }, + }.Do() + require.Nil(t, err) + + // Unmarshal the response + var response routes.AccountsCreateResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + // Check the result's contents + require.False(t, response.Success) + require.Equal(t, "Invalid invitation token", response.Message) +} + +func TestAccountsCreateClassic(t *testing.T) { + const ( + username = "jeremy_was_invited" + password = "potato" + ) + + // POST /accounts - classic + createClassicResult, err := goreq.Request{ + Method: "POST", + Uri: server.URL + "/accounts", + ContentType: "application/json", + Body: routes.AccountsCreateRequest{ + Username: username + "classic", + Password: password, + AltEmail: "something@example.com", + }, + }.Do() + require.Nil(t, err) + + // Unmarshal the response + var createClassicResponse routes.AccountsCreateResponse + err = createClassicResult.Body.FromJsonTo(&createClassicResponse) + require.Nil(t, err) + + // Check the result's contents + require.True(t, createClassicResponse.Success) + require.Equal(t, "A new account was successfully created, you should receive a confirmation email soon™.", createClassicResponse.Message) + require.NotEmpty(t, createClassicResponse.Account.ID) +} + +func TestAccountsCreateClassicDisabled(t *testing.T) { + const ( + username = "jeremy_was_invited" + password = "potato" + ) + + env.Config.ClassicRegistration = false + + // POST /accounts - classic + createClassicResult, err := goreq.Request{ + Method: "POST", + Uri: server.URL + "/accounts", + ContentType: "application/json", + Body: routes.AccountsCreateRequest{ + Username: username + "classic", + Password: password, + AltEmail: "something@example.com", + }, + }.Do() + require.Nil(t, err) + + // Unmarshal the response + var createClassicResponse routes.AccountsCreateResponse + err = createClassicResult.Body.FromJsonTo(&createClassicResponse) + require.Nil(t, err) + + // Check the result's contents + require.False(t, createClassicResponse.Success) + require.Equal(t, "Classic registration is disabled", createClassicResponse.Message) + + env.Config.ClassicRegistration = true +} + +func TestAccountsCreateQueue(t *testing.T) { + // POST /accounts - queue + result, err := goreq.Request{ + Method: "POST", + Uri: server.URL + "/accounts", + ContentType: "application/json", + Body: routes.AccountsCreateRequest{ + AltEmail: "something@example.com", + }, + }.Do() + require.Nil(t, err) + + // Unmarshal the response + var response routes.AccountsCreateResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + // Check the result's contents + require.False(t, response.Success) + require.Equal(t, "Sorry, not implemented yet", response.Message) +} + +func TestAccountsPrepareToken(t *testing.T) { + // log in as mr jeremy potato + const ( + username = "jeremy" + password = "potato" + ) + // POST /accounts - classic + request, err := goreq.Request{ + Method: "POST", + Uri: server.URL + "/tokens", + ContentType: "application/json", + Body: routes.TokensCreateRequest{ + Username: username, + Password: password, + Type: "auth", + }, + }.Do() + require.Nil(t, err) + + // Unmarshal the response + var response routes.TokensCreateResponse + err = request.Body.FromJsonTo(&response) + require.Nil(t, err) + + // Check the result's contents + require.True(t, response.Success) + require.Equal(t, "Authentication successful", response.Message) + require.NotEmpty(t, response.Token.ID) + + // Populate the global token variable + authToken = response.Token.ID +} + +func TestAccountsList(t *testing.T) { + // GET /accounts + request := goreq.Request{ + Method: "GET", + Uri: server.URL + "/accounts", + } + request.AddHeader("Authorization", "Bearer "+authToken) + result, err := request.Do() + require.Nil(t, err) + + // Unmarshal the response + var response routes.AccountsListResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + // Check the result's contents + require.False(t, response.Success) + require.Equal(t, "Sorry, not implemented yet", response.Message) +} + +func TestAccountsGetMe(t *testing.T) { + // GET /accounts/me + request := goreq.Request{ + Method: "GET", + Uri: server.URL + "/accounts/me", + } + request.AddHeader("Authorization", "Bearer "+authToken) + result, err := request.Do() + require.Nil(t, err) + + // Unmarshal the response + var response routes.AccountsGetResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + // Check the result's contents + require.True(t, response.Success) + require.Equal(t, "jeremy", response.Account.Name) +} + +func TestAccountsGetNotMe(t *testing.T) { + request := goreq.Request{ + Method: "GET", + Uri: server.URL + "/accounts/not-me", + } + request.AddHeader("Authorization", "Bearer "+authToken) + result, err := request.Do() + require.Nil(t, err) + + // Unmarshal the response + var response routes.AccountsGetResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + // Check the result's contents + require.False(t, response.Success) + require.Equal(t, `Only the "me" user is implemented`, response.Message) +} + +func TestAccountUpdateMe(t *testing.T) { + // PUT /accounts/me + request := goreq.Request{ + Method: "PUT", + Uri: server.URL + "/accounts/me", + ContentType: "application/json", + Body: &routes.AccountsUpdateRequest{ + CurrentPassword: "potato", + NewPassword: "cabbage", + AltEmail: "john.cabbage@example.com", + }, + } + request.AddHeader("Authorization", "Bearer "+authToken) + result, err := request.Do() + require.Nil(t, err) + + // Unmarshal the response + var response routes.AccountsUpdateResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + // Check the result's contents + require.Equal(t, "Your account has been successfully updated", response.Message) + require.True(t, response.Success) + require.Equal(t, "jeremy", response.Account.Name) + require.Equal(t, "john.cabbage@example.com", response.Account.AltEmail) +} + +func TestAccountUpdateInvalid(t *testing.T) { + // PUT /accounts/me + request := goreq.Request{ + Method: "PUT", + Uri: server.URL + "/accounts/me", + ContentType: "application/json", + Body: "123123123!@#!@#!@#", + } + request.AddHeader("Authorization", "Bearer "+authToken) + result, err := request.Do() + require.Nil(t, err) + + // Unmarshal the response + var response routes.AccountsUpdateResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + // Check the result's contents + require.Equal(t, "Invalid input format", response.Message) + require.False(t, response.Success) +} + +func TestAccountUpdateNotMe(t *testing.T) { + // PUT /accounts/me + request := goreq.Request{ + Method: "PUT", + Uri: server.URL + "/accounts/not-me", + ContentType: "application/json", + Body: &routes.AccountsUpdateRequest{ + CurrentPassword: "potato", + NewPassword: "cabbage", + AltEmail: "john.cabbage@example.com", + }, + } + request.AddHeader("Authorization", "Bearer "+authToken) + result, err := request.Do() + require.Nil(t, err) + + // Unmarshal the response + var response routes.AccountsUpdateResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + // Check the result's contents + require.Equal(t, `Only the "me" user is implemented`, response.Message) + require.False(t, response.Success) +} + +func TestAccountUpdateMeInvalidPassword(t *testing.T) { + // PUT /accounts/me + request := goreq.Request{ + Method: "PUT", + Uri: server.URL + "/accounts/me", + ContentType: "application/json", + Body: &routes.AccountsUpdateRequest{ + CurrentPassword: "potato2", + NewPassword: "cabbage", + AltEmail: "john.cabbage@example.com", + }, + } + request.AddHeader("Authorization", "Bearer "+authToken) + result, err := request.Do() + require.Nil(t, err) + + // Unmarshal the response + var response routes.AccountsUpdateResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + // Check the result's contents + require.Equal(t, "Invalid current password", response.Message) + require.False(t, response.Success) +} + +func TestAccountsWipeDataNotMe(t *testing.T) { + // POST /accounts/me/wipe-data + request := goreq.Request{ + Method: "POST", + Uri: server.URL + "/accounts/not-me/wipe-data", + } + request.AddHeader("Authorization", "Bearer "+authToken) + result, err := request.Do() + require.Nil(t, err) + + // Unmarshal the response + var response routes.AccountsWipeDataResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + // Check the result's contents + require.Equal(t, `Only the "me" user is implemented`, response.Message) + require.False(t, response.Success) +} + +func TestAccountsWipeData(t *testing.T) { + // POST /accounts/me/wipe-data + request := goreq.Request{ + Method: "POST", + Uri: server.URL + "/accounts/me/wipe-data", + } + request.AddHeader("Authorization", "Bearer "+authToken) + result, err := request.Do() + require.Nil(t, err) + + // Unmarshal the response + var response routes.AccountsWipeDataResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + // Check the result's contents + require.Equal(t, "Your account has been successfully wiped", response.Message) + require.True(t, response.Success) +} + +func TestAccountsDeleteNotMe(t *testing.T) { + // Prepare a token + token := models.Token{ + Resource: models.MakeResource(accountID, "test invite token"), + Type: "auth", + } + token.ExpireSoon() + + err := env.Tokens.Insert(token) + require.Nil(t, err) + + // DELETE /accounts/me + request := goreq.Request{ + Method: "DELETE", + Uri: server.URL + "/accounts/not-me", + } + request.AddHeader("Authorization", "Bearer "+token.ID) + result, err := request.Do() + require.Nil(t, err) + + // Unmarshal the response + var response routes.AccountsWipeDataResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + // Check the result's contents + require.Equal(t, `Only the "me" user is implemented`, response.Message) + require.False(t, response.Success) +} + +func TestAccountsDelete(t *testing.T) { + // Prepare a token + token := models.Token{ + Resource: models.MakeResource(accountID, "test invite token"), + Type: "auth", + } + token.ExpireSoon() + + err := env.Tokens.Insert(token) + require.Nil(t, err) + + // DELETE /accounts/me + request := goreq.Request{ + Method: "DELETE", + Uri: server.URL + "/accounts/me", + } + request.AddHeader("Authorization", "Bearer "+token.ID) + result, err := request.Do() + require.Nil(t, err) + + // Unmarshal the response + var response routes.AccountsWipeDataResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + // Check the result's contents + require.Equal(t, "Your account has been successfully deleted", response.Message) + require.True(t, response.Success) +} diff --git a/routes/contacts.go b/routes/contacts.go index 5d8e18a..0e46ef4 100644 --- a/routes/contacts.go +++ b/routes/contacts.go @@ -30,7 +30,7 @@ func ContactsList(c web.C, w http.ResponseWriter, r *http.Request) { "error": err, }).Error("Unable to fetch contacts") - utils.JSONResponse(w, 500, &AccountsDeleteResponse{ + utils.JSONResponse(w, 500, &ContactsListResponse{ Success: false, Message: "Internal error (code CO/LI/01)", }) @@ -38,7 +38,7 @@ func ContactsList(c web.C, w http.ResponseWriter, r *http.Request) { } utils.JSONResponse(w, 501, &ContactsListResponse{ - Success: false, + Success: true, Contacts: &contacts, }) } @@ -48,8 +48,8 @@ type ContactsCreateRequest struct { Data string `json:"data" schema:"data"` Name string `json:"name" schema:"name"` Encoding string `json:"encoding" schema:"encoding"` - VersionMajor *int `json:"version_major" schema:"version_major"` - VersionMinor *int `json:"version_minor" schema:"version_minor"` + VersionMajor int `json:"version_major" schema:"version_major"` + VersionMinor int `json:"version_minor" schema:"version_minor"` } // ContactsCreateResponse contains the result of the ContactsCreate request. @@ -80,7 +80,7 @@ func ContactsCreate(c web.C, w http.ResponseWriter, r *http.Request) { session := c.Env["token"].(*models.Token) // Ensure that the input data isn't empty - if input.Data != "" || input.Name != "" || input.Encoding != "" || input.VersionMajor != nil || input.VersionMinor != nil { + if input.Data == "" || input.Name == "" || input.Encoding == "" { utils.JSONResponse(w, 400, &ContactsCreateResponse{ Success: false, Message: "Invalid request", @@ -94,8 +94,8 @@ func ContactsCreate(c web.C, w http.ResponseWriter, r *http.Request) { Encoding: input.Encoding, Data: input.Data, Schema: "contact", - VersionMajor: *input.VersionMajor, - VersionMinor: *input.VersionMinor, + VersionMajor: input.VersionMajor, + VersionMinor: input.VersionMinor, }, Resource: models.MakeResource(session.Owner, input.Name), } @@ -115,7 +115,7 @@ func ContactsCreate(c web.C, w http.ResponseWriter, r *http.Request) { utils.JSONResponse(w, 201, &ContactsCreateResponse{ Success: true, - Message: "A new account was successfully created", + Message: "A new contact was successfully created", Contact: contact, }) } diff --git a/routes/contacts_test.go b/routes/contacts_test.go new file mode 100644 index 0000000..e2a5ee7 --- /dev/null +++ b/routes/contacts_test.go @@ -0,0 +1,378 @@ +package routes_test + +import ( + "testing" + + "github.com/franela/goreq" + "github.com/stretchr/testify/require" + + "github.com/lavab/api/env" + "github.com/lavab/api/models" + "github.com/lavab/api/routes" +) + +var ( + contactID string + notOwnedContactID string +) + +func TestContactsPrepareAccount(t *testing.T) { + const ( + username = "jeremy-contacts" + password = "potato" + ) + + inviteToken := models.Token{ + Resource: models.MakeResource("", "test invite token"), + Type: "invite", + } + inviteToken.ExpireSoon() + + err := env.Tokens.Insert(inviteToken) + require.Nil(t, err) + + result1, err := goreq.Request{ + Method: "POST", + Uri: server.URL + "/accounts", + ContentType: "application/json", + Body: routes.AccountsCreateRequest{ + Username: username, + Password: password, + Token: inviteToken.ID, + }, + }.Do() + require.Nil(t, err) + + var response1 routes.AccountsCreateResponse + err = result1.Body.FromJsonTo(&response1) + require.Nil(t, err) + + require.True(t, response1.Success) + require.Equal(t, "A new account was successfully created", response1.Message) + require.NotEmpty(t, response1.Account.ID) + + accountID = response1.Account.ID + + request2, err := goreq.Request{ + Method: "POST", + Uri: server.URL + "/tokens", + ContentType: "application/json", + Body: routes.TokensCreateRequest{ + Username: username, + Password: password, + Type: "auth", + }, + }.Do() + require.Nil(t, err) + + var response2 routes.TokensCreateResponse + err = request2.Body.FromJsonTo(&response2) + require.Nil(t, err) + + require.True(t, response2.Success) + require.Equal(t, "Authentication successful", response2.Message) + require.NotEmpty(t, response2.Token.ID) + + authToken = response2.Token.ID +} + +func TestContactsCreate(t *testing.T) { + request := goreq.Request{ + Method: "POST", + Uri: server.URL + "/contacts", + ContentType: "application/json", + Body: routes.ContactsCreateRequest{ + Data: "random stuff", + Name: "John Doe", + Encoding: "json", + VersionMajor: 1, + VersionMinor: 0, + }, + } + request.AddHeader("Authorization", "Bearer "+authToken) + result, err := request.Do() + require.Nil(t, err) + + var response routes.ContactsCreateResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + require.Equal(t, "A new contact was successfully created", response.Message) + require.True(t, response.Success) + require.NotEmpty(t, response.Contact.ID) + + contactID = response.Contact.ID +} + +func TestContactsCreateMissingParts(t *testing.T) { + request := goreq.Request{ + Method: "POST", + Uri: server.URL + "/contacts", + ContentType: "application/json", + Body: routes.ContactsCreateRequest{ + Data: "random stuff", + Name: "John Doe", + Encoding: "", + VersionMajor: 1, + VersionMinor: 0, + }, + } + request.AddHeader("Authorization", "Bearer "+authToken) + result, err := request.Do() + require.Nil(t, err) + + var response routes.ContactsCreateResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + require.Equal(t, "Invalid request", response.Message) + require.False(t, response.Success) +} + +func TestContactsCreateInvalid(t *testing.T) { + request := goreq.Request{ + Method: "POST", + Uri: server.URL + "/contacts", + ContentType: "application/json", + Body: "!@#!@#!@#", + } + request.AddHeader("Authorization", "Bearer "+authToken) + result, err := request.Do() + require.Nil(t, err) + + var response routes.ContactsCreateResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + require.Equal(t, "Invalid input format", response.Message) + require.False(t, response.Success) +} + +func TestContactsList(t *testing.T) { + request := goreq.Request{ + Method: "GET", + Uri: server.URL + "/contacts", + } + request.AddHeader("Authorization", "Bearer "+authToken) + result, err := request.Do() + require.Nil(t, err) + + var response routes.ContactsListResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + require.True(t, response.Success) + require.True(t, len(*response.Contacts) > 0) +} + +func TestContactsGet(t *testing.T) { + request := goreq.Request{ + Method: "GET", + Uri: server.URL + "/contacts/" + contactID, + } + request.AddHeader("Authorization", "Bearer "+authToken) + result, err := request.Do() + require.Nil(t, err) + + var response routes.ContactsGetResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + require.True(t, response.Success) + require.Equal(t, "John Doe", response.Contact.Name) +} + +func TestContactsGetNotOwned(t *testing.T) { + contact := &models.Contact{ + Encrypted: models.Encrypted{ + Encoding: "json", + Data: "carp", + Schema: "contact", + VersionMajor: 1, + VersionMinor: 0, + }, + Resource: models.MakeResource("not", "Carpeus Caesar"), + } + + err := env.Contacts.Insert(contact) + require.Nil(t, err) + + notOwnedContactID = contact.ID + + request := goreq.Request{ + Method: "GET", + Uri: server.URL + "/contacts/" + contact.ID, + } + request.AddHeader("Authorization", "Bearer "+authToken) + result, err := request.Do() + require.Nil(t, err) + + var response routes.ContactsGetResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + require.False(t, response.Success) + require.Equal(t, "Contact not found", response.Message) +} + +func TestContactsGetWrongID(t *testing.T) { + request := goreq.Request{ + Method: "GET", + Uri: server.URL + "/contacts/gibberish", + } + request.AddHeader("Authorization", "Bearer "+authToken) + result, err := request.Do() + require.Nil(t, err) + + var response routes.ContactsGetResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + require.False(t, response.Success) + require.Equal(t, "Contact not found", response.Message) +} + +func TestContactsUpdate(t *testing.T) { + request := goreq.Request{ + Method: "PUT", + Uri: server.URL + "/contacts/" + contactID, + ContentType: "application/json", + Body: routes.ContactsUpdateRequest{ + Data: "random stuff2", + Name: "John Doez", + Encoding: "json", + VersionMajor: 1, + VersionMinor: 0, + }, + } + request.AddHeader("Authorization", "Bearer "+authToken) + result, err := request.Do() + require.Nil(t, err) + + var response routes.ContactsUpdateResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + require.True(t, response.Success) + require.Equal(t, "John Doez", response.Contact.Name) +} + +func TestContactsUpdateInvalid(t *testing.T) { + request := goreq.Request{ + Method: "PUT", + Uri: server.URL + "/contacts/" + contactID, + ContentType: "application/json", + Body: "123123!@#!@#", + } + request.AddHeader("Authorization", "Bearer "+authToken) + result, err := request.Do() + require.Nil(t, err) + + var response routes.ContactsUpdateResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + require.False(t, response.Success) + require.Equal(t, "Invalid input format", response.Message) +} + +func TestContactsUpdateNotOwned(t *testing.T) { + request := goreq.Request{ + Method: "PUT", + Uri: server.URL + "/contacts/" + notOwnedContactID, + ContentType: "application/json", + Body: routes.ContactsUpdateRequest{ + Data: "random stuff2", + Name: "John Doez", + Encoding: "json", + VersionMajor: 1, + VersionMinor: 0, + }, + } + request.AddHeader("Authorization", "Bearer "+authToken) + result, err := request.Do() + require.Nil(t, err) + + var response routes.ContactsUpdateResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + require.False(t, response.Success) + require.Equal(t, "Contact not found", response.Message) +} + +func TestContactsUpdateNotExisting(t *testing.T) { + request := goreq.Request{ + Method: "PUT", + Uri: server.URL + "/contacts/gibberish", + ContentType: "application/json", + Body: routes.ContactsUpdateRequest{ + Data: "random stuff2", + Name: "John Doez", + Encoding: "json", + VersionMajor: 1, + VersionMinor: 0, + }, + } + request.AddHeader("Authorization", "Bearer "+authToken) + result, err := request.Do() + require.Nil(t, err) + + var response routes.ContactsUpdateResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + require.False(t, response.Success) + require.Equal(t, "Contact not found", response.Message) +} + +func TestContactsDelete(t *testing.T) { + request := goreq.Request{ + Method: "DELETE", + Uri: server.URL + "/contacts/" + contactID, + } + request.AddHeader("Authorization", "Bearer "+authToken) + result, err := request.Do() + require.Nil(t, err) + + var response routes.ContactsDeleteResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + require.True(t, response.Success) + require.Equal(t, "Contact successfully removed", response.Message) +} + +func TestContactsDeleteNotOwned(t *testing.T) { + request := goreq.Request{ + Method: "DELETE", + Uri: server.URL + "/contacts/" + notOwnedContactID, + } + request.AddHeader("Authorization", "Bearer "+authToken) + result, err := request.Do() + require.Nil(t, err) + + var response routes.ContactsDeleteResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + require.False(t, response.Success) + require.Equal(t, "Contact not found", response.Message) +} + +func TestContactsDeleteNotExisting(t *testing.T) { + request := goreq.Request{ + Method: "DELETE", + Uri: server.URL + "/contacts/gibberish", + } + request.AddHeader("Authorization", "Bearer "+authToken) + result, err := request.Do() + require.Nil(t, err) + + var response routes.ContactsDeleteResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + require.False(t, response.Success) + require.Equal(t, "Contact not found", response.Message) +} diff --git a/routes/hello_test.go b/routes/hello_test.go new file mode 100644 index 0000000..7f05bf4 --- /dev/null +++ b/routes/hello_test.go @@ -0,0 +1,25 @@ +package routes_test + +import ( + "testing" + + "github.com/franela/goreq" + "github.com/stretchr/testify/require" + + "github.com/lavab/api/routes" +) + +func TestHello(t *testing.T) { + // Request the / route + helloResult, err := goreq.Request{ + Method: "GET", + Uri: server.URL, + }.Do() + require.Nil(t, err) + + // Unmarshal the response + var helloResponse routes.HelloResponse + err = helloResult.Body.FromJsonTo(&helloResponse) + require.Nil(t, err) + require.Equal(t, "Lavaboom API", helloResponse.Message) +} diff --git a/routes/init_test.go b/routes/init_test.go new file mode 100644 index 0000000..3c1ffb4 --- /dev/null +++ b/routes/init_test.go @@ -0,0 +1,68 @@ +package routes_test + +import ( + "net/http/httptest" + "time" + + "github.com/dancannon/gorethink" + + "github.com/lavab/api/env" + "github.com/lavab/api/setup" +) + +var ( + server *httptest.Server + accountID string + authToken string +) + +func init() { + // Mock data + env.Config = &env.Flags{ + APIVersion: "v0", + LogFormatterType: "text", + ForceColors: true, + + SessionDuration: 72, + ClassicRegistration: true, + + RethinkDBURL: "127.0.0.1:28015", + RethinkDBKey: "", + RethinkDBDatabase: "test", + } + + // Connect to the RethinkDB server + rdbSession, err := gorethink.Connect(gorethink.ConnectOpts{ + Address: env.Config.RethinkDBURL, + AuthKey: env.Config.RethinkDBKey, + MaxIdle: 10, + IdleTimeout: time.Second * 10, + }) + if err != nil { + panic("connecting to RethinkDB should not return an error, got " + err.Error()) + } + + // Clear the test database + err = gorethink.DbDrop("test").Exec(rdbSession) + if err != nil { + panic("removing the test database should not return an error, got " + err.Error()) + } + + // Disconnect + err = rdbSession.Close() + if err != nil { + panic("closing the RethinkDB session should not return an error, got " + err.Error()) + } + + // Prepare a new mux (initialize the API) + mux := setup.PrepareMux(env.Config) + if mux == nil { + panic("returned mux was nil") + } + + // Set up a new temporary HTTP test server + server = httptest.NewServer(mux) + if server == nil { + panic("returned httptest server was nil") + } +} diff --git a/routes/keys.go b/routes/keys.go index cbd4fce..980f705 100644 --- a/routes/keys.go +++ b/routes/keys.go @@ -1,224 +1,217 @@ -package routes - -import ( - "encoding/hex" - "fmt" - "net/http" - "strings" - - "github.com/Sirupsen/logrus" - "github.com/zenazn/goji/web" - "golang.org/x/crypto/openpgp" - - "github.com/lavab/api/env" - "github.com/lavab/api/models" - "github.com/lavab/api/utils" -) - -// KeysListResponse contains the result of the KeysList request -type KeysListResponse struct { - Success bool `json:"success"` - Message string `json:"message,omitempty"` - Keys *[]string `json:"keys,omitempty"` -} - -// KeysList responds with the list of keys assigned to the spiecified email -func KeysList(w http.ResponseWriter, r *http.Request) { - // Get the username from the GET query - user := r.URL.Query().Get("user") - if user == "" { - utils.JSONResponse(w, 409, &KeysListResponse{ - Success: false, - Message: "Invalid username", - }) - return - } - - // Find all keys owner by user - keys, err := env.Keys.FindByName(user) - if err != nil { - utils.JSONResponse(w, 500, &KeysListResponse{ - Success: false, - Message: "Internal server error (KE/LI/01)", - }) - return - } - - // Equivalent of _.keys(keys) in JavaScript with underscore.js - keyIDs := []string{} - for _, key := range keys { - keyIDs = append(keyIDs, key.ID) - } - - // Respond with list of keys - utils.JSONResponse(w, 200, &KeysListResponse{ - Success: true, - Keys: &keyIDs, - }) -} - -// KeysCreateRequest contains the data passed to the KeysCreate endpoint. -type KeysCreateRequest struct { - Key string `json:"key" schema:"key"` // gpg armored key - Image string `json:"image" schema:"image"` // todo -} - -// KeysCreateResponse contains the result of the KeysCreate request. -type KeysCreateResponse struct { - Success bool `json:"success"` - Message string `json:"message"` - Key *models.Key `json:"key,omitempty"` -} - -// KeysCreate appens a new key to the server -func KeysCreate(c web.C, w http.ResponseWriter, r *http.Request) { - // Decode the request - var input KeysCreateRequest - err := utils.ParseRequest(r, &input) - if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err, - }).Warn("Unable to decode a request") - - utils.JSONResponse(w, 409, &KeysCreateResponse{ - Success: false, - Message: "Invalid input format", - }) - return - } - - // Get the session - session := c.Env["token"].(*models.Token) - - // Parse the armored key - entityList, err := openpgp.ReadArmoredKeyRing(strings.NewReader(input.Key)) - if err != nil { - utils.JSONResponse(w, 409, &KeysCreateResponse{ - Success: false, - Message: "Invalid key format", - }) - - env.Log.WithFields(logrus.Fields{ - "error": err, - "list": entityList, - }).Warn("Cannot parse an armored key") - return - } - - // Get the account from db - account, err := env.Accounts.GetAccount(session.Owner) - if err != nil { - utils.JSONResponse(w, 500, &KeysCreateResponse{ - Success: false, - Message: "Internal server error - KE/CR/01", - }) - - env.Log.WithFields(logrus.Fields{ - "error": err, - "id": session.Owner, - }).Error("Cannot fetch user from database") - return - } - - // Let's hope that the user is capable of sending proper armored keys - publicKey := entityList[0] - - // Encode the fingerprint - id := hex.EncodeToString(publicKey.PrimaryKey.Fingerprint[:]) - - // Get the key's bit length - should not return an error - bitLength, _ := publicKey.PrimaryKey.BitLength() - - // Allocate a new key - key := &models.Key{ - Resource: models.MakeResource( - session.Owner, - fmt.Sprintf( - "%d/%s public key", - bitLength, - publicKey.PrimaryKey.KeyIdString(), - ), - ), - OwnerName: account.Name, - Key: input.Key, - KeyID: publicKey.PrimaryKey.KeyIdString(), - KeyIDShort: publicKey.PrimaryKey.KeyIdShortString(), - } - - // Update id as we can't do it directly during allocation - key.ID = id - - // Try to insert it into the database - if err := env.Keys.Insert(key); err != nil { - utils.JSONResponse(w, 500, &KeysCreateResponse{ - Success: false, - Message: "Internal server error - KE/CR/02", - }) - - env.Log.WithFields(logrus.Fields{ - "error": err, - }).Error("Could not insert a key to the database") - return - } - - // Return the inserted key - utils.JSONResponse(w, 201, &KeysCreateResponse{ - Success: true, - Message: "A new key has been successfully inserted", - Key: key, - }) -} - -// KeysGetResponse contains the result of the KeysGet request. -type KeysGetResponse struct { - Success bool `json:"success"` - Message string `json:"message,omitempty"` - Key *models.Key `json:"key,omitempty"` -} - -// KeysGet does *something* - TODO -func KeysGet(c web.C, w http.ResponseWriter, r *http.Request) { - // Get ID from the passed URL params - id, ok := c.URLParams["id"] - if !ok { - utils.JSONResponse(w, 404, &KeysGetResponse{ - Success: false, - Message: "Requested key does not exist on our server", - }) - return - } - - // Fetch the requested key from the database - key, err := env.Keys.FindByFingerprint(id) - if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err, - }).Warn("Unable to fetch the requested key from the database") - - utils.JSONResponse(w, 404, &KeysGetResponse{ - Success: false, - Message: "Requested key does not exist on our server", - }) - return - } - - // Return the requested key - utils.JSONResponse(w, 200, &KeysGetResponse{ - Success: true, - Key: key, - }) -} - -// KeysVoteResponse contains the result of the KeysVote request. -type KeysVoteResponse struct { - Success bool `json:"success"` - Message string `json:"message"` -} - -// KeysVote does *something* - TODO -func KeysVote(w http.ResponseWriter, r *http.Request) { - utils.JSONResponse(w, 501, &KeysVoteResponse{ - Success: false, - Message: "Sorry, not implemented yet", - }) -} +package routes + +import ( + "encoding/hex" + "fmt" + "net/http" + "strings" + + "github.com/Sirupsen/logrus" + "github.com/zenazn/goji/web" + "golang.org/x/crypto/openpgp" + + "github.com/lavab/api/env" + "github.com/lavab/api/models" + "github.com/lavab/api/utils" +) + +// KeysListResponse contains the result of the KeysList request +type KeysListResponse struct { + Success bool `json:"success"` + Message string `json:"message,omitempty"` + Keys *[]string `json:"keys,omitempty"` +} + +// KeysList responds with the list of keys assigned to the spiecified email +func KeysList(w http.ResponseWriter, r *http.Request) { + // Get the username from the GET query + user := r.URL.Query().Get("user") + if user == "" { + utils.JSONResponse(w, 409, &KeysListResponse{ + Success: false, + Message: "Invalid username", + }) + return + } + + // Find all keys owner by user + keys, err := env.Keys.FindByName(user) + if err != nil { + utils.JSONResponse(w, 500, &KeysListResponse{ + Success: false, + Message: "Internal server error (KE/LI/01)", + }) + return + } + + // Equivalent of _.keys(keys) in JavaScript with underscore.js + keyIDs := []string{} + for _, key := range keys { + keyIDs = append(keyIDs, key.ID) + } + + // Respond with list of keys + utils.JSONResponse(w, 200, &KeysListResponse{ + Success: true, + Keys: &keyIDs, + }) +} + +// KeysCreateRequest contains the data passed to the KeysCreate endpoint. +type KeysCreateRequest struct { + Key string `json:"key" schema:"key"` // gpg armored key + Image string `json:"image" schema:"image"` // todo +} + +// KeysCreateResponse contains the result of the KeysCreate request. +type KeysCreateResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + Key *models.Key `json:"key,omitempty"` +} + +// KeysCreate appens a new key to the server +func KeysCreate(c web.C, w http.ResponseWriter, r *http.Request) { + // Decode the request + var input KeysCreateRequest + err := utils.ParseRequest(r, &input) + if err != nil { + env.Log.WithFields(logrus.Fields{ + "error": err, + }).Warn("Unable to decode a request") + + utils.JSONResponse(w, 409, &KeysCreateResponse{ + Success: false, + Message: "Invalid input format", + }) + return + } + + // Get the session + session := c.Env["token"].(*models.Token) + + // Parse the armored key + entityList, err := openpgp.ReadArmoredKeyRing(strings.NewReader(input.Key)) + if err != nil { + utils.JSONResponse(w, 409, &KeysCreateResponse{ + Success: false, + Message: "Invalid key format", + }) + + env.Log.WithFields(logrus.Fields{ + "error": err, + "list": entityList, + }).Warn("Cannot parse an armored key") + return + } + + // Get the account from db + account, err := env.Accounts.GetAccount(session.Owner) + if err != nil { + utils.JSONResponse(w, 500, &KeysCreateResponse{ + Success: false, + Message: "Internal server error - KE/CR/01", + }) + + env.Log.WithFields(logrus.Fields{ + "error": err, + "id": session.Owner, + }).Error("Cannot fetch user from database") + return + } + + // Let's hope that the user is capable of sending proper armored keys + publicKey := entityList[0] + + // Encode the fingerprint + id := hex.EncodeToString(publicKey.PrimaryKey.Fingerprint[:]) + + // Get the key's bit length - should not return an error + bitLength, _ := publicKey.PrimaryKey.BitLength() + + // Allocate a new key + key := &models.Key{ + Resource: models.MakeResource( + session.Owner, + fmt.Sprintf( + "%d/%s public key", + bitLength, + publicKey.PrimaryKey.KeyIdString(), + ), + ), + OwnerName: account.Name, + Key: input.Key, + KeyID: publicKey.PrimaryKey.KeyIdString(), + KeyIDShort: publicKey.PrimaryKey.KeyIdShortString(), + } + + // Update id as we can't do it directly during allocation + key.ID = id + + // Try to insert it into the database + if err := env.Keys.Insert(key); err != nil { + utils.JSONResponse(w, 500, &KeysCreateResponse{ + Success: false, + Message: "Internal server error - KE/CR/02", + }) + + env.Log.WithFields(logrus.Fields{ + "error": err, + }).Error("Could not insert a key to the database") + return + } + + // Return the inserted key + utils.JSONResponse(w, 201, &KeysCreateResponse{ + Success: true, + Message: "A new key has been successfully inserted", + Key: key, + }) +} + +// KeysGetResponse contains the result of the KeysGet request. +type KeysGetResponse struct { + Success bool `json:"success"` + Message string `json:"message,omitempty"` + Key *models.Key `json:"key,omitempty"` +} + +// KeysGet does *something* - TODO +func KeysGet(c web.C, w http.ResponseWriter, r *http.Request) { + // Get ID from the passed URL params + id := c.URLParams["id"] + + // Fetch the requested key from the database + key, err := env.Keys.FindByFingerprint(id) + if err != nil { + env.Log.WithFields(logrus.Fields{ + "error": err, + }).Warn("Unable to fetch the requested key from the database") + + utils.JSONResponse(w, 404, &KeysGetResponse{ + Success: false, + Message: "Requested key does not exist on our server", + }) + return + } + + // Return the requested key + utils.JSONResponse(w, 200, &KeysGetResponse{ + Success: true, + Key: key, + }) +} + +// KeysVoteResponse contains the result of the KeysVote request. +type KeysVoteResponse struct { + Success bool `json:"success"` + Message string `json:"message"` +} + +// KeysVote does *something* - TODO +func KeysVote(w http.ResponseWriter, r *http.Request) { + utils.JSONResponse(w, 501, &KeysVoteResponse{ + Success: false, + Message: "Sorry, not implemented yet", + }) +} diff --git a/routes/keys_test.go b/routes/keys_test.go new file mode 100644 index 0000000..32b5e81 --- /dev/null +++ b/routes/keys_test.go @@ -0,0 +1,195 @@ +package routes_test + +import ( + "testing" + + "github.com/franela/goreq" + "github.com/lavab/api/routes" + "github.com/stretchr/testify/require" +) + +var ( + keyID string +) + +func TestKeysCreate(t *testing.T) { + request := goreq.Request{ + Method: "POST", + Uri: server.URL + "/keys", + ContentType: "application/json", + Body: routes.KeysCreateRequest{ + Key: `-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1 + +mQINBFR6JFoBEADoLOVi5NEkIELYOIfOsztAuPqNPiJcDXCsKuprjNj7n2vxyNim +WbArRZ4TJereG0H2skCQlKMx26EiHHdK3je4i6erD+OT4NolAsxVsl4PpkEDZnzz +tIwVb7FymahIrqwP9YPrXc0tr07HgnE3+it828ZJlCMfGUgJJrn12p+UetlBoFwr +OEgaCl4fOfAuUQUzD156AGV/S0H4ge8H7yngSxNTMCqypX6SaX+O0uhKqa3CxiiG +HxIGo+lNdM72Xm3Ym9sNKtfsflkqZdlWfdpit1mgveZMx2CpuYI1aS+FRzQczCDn +fDnSVqErIWUv64daC5qU3pPWjqRuOr4WXEdxXSCgi2oXVP+2hVyqgPk6ch64TodR +lKxFN2wvrJVYJd/5XQrojBtf/F/ZnlYq0rze+snZ5R1lBMZMU2oBnWtRQMSO/+8b +iHY/7mjyT+LGLXhbGGmgtycYsuujR54Smtzx1tc7CsoVLJ3JB4629YT6RtDnd85R +f7oUnjtd714e6k6zLIkppsSDse8WOPGtnfHxswrNRGnEPFYxQhCN+PbYdwGmSfmA +kzoJFumJF8KIXflGBZ0s2JdAx4G1aMhPR3rUNiJdh+DXXseLn/PAbDj2O4uMVi5F +/ai6U/vhNOatrt5syOwWZnShuIBj5VwwyJOdGjC9uwYrfocDtx7IdbaokQARAQAB +tCFQaW90ciBaZHVuaWFrIDxwaW90ckB6ZHVuaWFrLm5ldD6JAjgEEwECACIFAlR6 +JFoCGwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEN9g3PR+HyAlZigP/3H2 +l9icK0tazF5B4jcPaKJ4cToe/XiTU1eNNzTGftlbtCgb2e2TMuzcY7LpiK3zHO5z +0NlVKWxAoD7JHEaG5vwL74gB1324VbW08dWcz/a/jMyTAUhGIZ1WBIJGa9dVkN98 +GZp6i8q2DfsvflQI5Q9s3+Y6nbl2FEDFc3U+UXyN3M7x94NEc+3BUPvds/CwD/L0 +rjatqusCf1lo2GNZvVcoluerKjSR0/LryTbQwSlW0rDIVAoc5AB1ezpJKfW6O22i +4h8MpNGNJ3XVrMIX4/Tu4ESE75WQSVqThd1Zy3y9bVvhL8UxKV3qviuBRDtlk/7N +QznUBTJ0RFegebTDp6+jVaVt+RBJg8rnwXOT0iSEBionCjjuIWX7hzM3mRg8FnnJ +RUudJxN2b1mJHKCHEG3/SIbl6m32HesJahfNnmGV8xs7YpZWHQU+DXoTJN8+t/2E +kZ7+4X38jdWfLfw4Z+Cb3J+J4yf0uipUQ8+6f7zm2p0BINlt5TQczZpWYQolhKoK +Xhd+Sd2XieaAkxUQqaYjCbr5fC5QouWYlwqnghCVSs1MLCPdHDI2FOXB5Sh8hOHN +sxar+5r9iWLkAvr5k+QoR8fQgarIQKcXQc+NQR65D8eneGo/apVknvRVMLrtC1ZI +QLi8aLMFaM6HReXsHD6PJUsuuHys2fhT+6vD4ujjuQINBFR6JFoBEADMa8xp8O1W +WvRxBZ0Bd0EOm+znhCsDhdHxrq3x74k3229NVJ42tfRunegP+s+/nFQuSV/FXxiL +NFb7cfTL2ZlibNbOwbZ6RQ66BdPaBKyIc0QdIsaR/+ehGqbG0dN1aAiQJBustPzX +RQJBhzHKx4FpdJLrFppe5JLp2pcmI9CoMHdirIh3uFF85sNBTa0MAHNBHzXBeZbv +jZDCxTkFBPmUEbNiUWDOPDQnZlJAG9VvXzSLilsZ4Cgj/jN0/MUJ+vEOb1NvOWNH +Wo0/uFqmMhAFHxFSUETnZ4Q/6ZU2bdCeAp9uo1oEFvaEbmRdW1BkjMOXqJ4V5bXj +p9qREraEargj3+FKQHIiKDEz6p4C9y0RsJROIj8oZmvZsynzsnrmU5Gme5V8a4sS +ruPkm3kmdPCWq1SSZ/3V293NnE73KKdy6XinuyZBWVN1y8jSd/lJpyIZzIIMAQSp +OwWBYnVwTIlbFi0Ad1BGvMMSCM15AdrN9Ywb7xfnlkXEMHTQk4czwJUDKYodIw1u +KnGm/N/SPlgm1sk59rlMTQk0/TFT6KsYEoDdEJP934lldG+11vgpcicV8owM0AQ4 +PYtVTKhHv7QNK0FCIHWIWq/QMLJn73X7kotgLB/1M94eTgcWasg4ENI/ZCCRelnL +6cs4Ggo4/j/bd5QhogdiJYHUlEDqUL+a0QARAQABiQIfBBgBAgAJBQJUeiRaAhsM +AAoJEN9g3PR+HyAled0P/0J9gp48UOSWmkoMOPbGCIyABYMmaoDKdYYf1rToP3wp +O2nOwG48ZFW9Q4r6LAiOmPjPtMsvjtFeHDQ5FjnXpbFI2NBn3YwB2fulim8TZL03 +SvpiZD7TUiZKmUAOmVPoZJ+GUIE9lJtBrlOS5n0TkhmS14G3xPlex7jdJ63JFmME +XZ9gDcgUOzG7pSneCYyHOLKGwTmLV3HXUSIAm/8bW2xJ7g+j9qr/c78D8ThUY+I0 +0edCq+tL5rpnPYIusI3lzh4xeSMSSVCKB+Fhz9DFdD6pZC6E6KWlaoUgw1DdvfFC +KFrEhGFPu80Y7zl77nME9Yg9JYrKlISZHtbT8mDduOXlJIyZxsIlg/bDhsN38HOE +3ZoAsJh/8Ui44b58x/u4P9uKDroCua/6sOb0JFuxPNZHc7Sjdy1S0md7YEW3vFyT +1H1XzRAOPLwJFoz4ymRz9COHTyzExycr/TIjoBG7v1nYOGUdqaTNU2/802LRQaE2 +eUftQWTTiFoES4Z0vTKmKwq3CoP80Z5zTrcQf8CdMmTd9bu9kE3AvrK6OD0amxKw +LNHuuVgP/KuG0U4M8A641mUjCt0ZvtDCcAgO90cQKdHsuiCkX/wFYGg+lCzwjtRZ +UZSWZtUmAO12vjmUwGtRbp5xfdbV+PmIBRYe0iikrykoBy+FLw9yHlSCoey2ih6W +=r/yh +-----END PGP PUBLIC KEY BLOCK-----`, + }, + } + request.AddHeader("Authorization", "Bearer "+authToken) + result, err := request.Do() + require.Nil(t, err) + + var response routes.KeysCreateResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + require.Equal(t, "A new key has been successfully inserted", response.Message) + require.True(t, response.Success) + require.NotEmpty(t, response.Key.ID) + + keyID = response.Key.ID +} + +func TestKeysCreateInvalid(t *testing.T) { + request := goreq.Request{ + Method: "POST", + Uri: server.URL + "/keys", + ContentType: "application/json", + Body: "!@#!@!@#", + } + request.AddHeader("Authorization", "Bearer "+authToken) + result, err := request.Do() + require.Nil(t, err) + + var response routes.KeysCreateResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + require.Equal(t, "Invalid input format", response.Message) + require.False(t, response.Success) +} + +func TestKeysCreateWrongKey(t *testing.T) { + request := goreq.Request{ + Method: "POST", + Uri: server.URL + "/keys", + ContentType: "application/json", + Body: routes.KeysCreateRequest{ + Key: `hbnjmvnbhvm nbhm jhbjmnghnbgjvgbhvf bgvmj gvhnft`, + }, + } + request.AddHeader("Authorization", "Bearer "+authToken) + result, err := request.Do() + require.Nil(t, err) + + var response routes.KeysCreateResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + require.Equal(t, "Invalid key format", response.Message) + require.False(t, response.Success) +} + +func TestKeysList(t *testing.T) { + request := goreq.Request{ + Method: "GET", + Uri: server.URL + "/keys?user=jeremy-contacts", + } + request.AddHeader("Authorization", "Bearer "+authToken) + result, err := request.Do() + require.Nil(t, err) + + var response routes.KeysListResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + require.True(t, response.Success) + require.True(t, len(*response.Keys) > 0) +} + +func TestKeysListNoUsername(t *testing.T) { + request := goreq.Request{ + Method: "GET", + Uri: server.URL + "/keys", + } + request.AddHeader("Authorization", "Bearer "+authToken) + result, err := request.Do() + require.Nil(t, err) + + var response routes.KeysListResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + require.False(t, response.Success) + require.Equal(t, "Invalid username", response.Message) +} + +func TestKeysGet(t *testing.T) { + request := goreq.Request{ + Method: "GET", + Uri: server.URL + "/keys/" + keyID, + } + request.AddHeader("Authorization", "Bearer "+authToken) + result, err := request.Do() + require.Nil(t, err) + + var response routes.KeysGetResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + require.True(t, response.Success) + require.Equal(t, keyID, response.Key.ID) +} + +func TestKeyGetInvalid(t *testing.T) { + request := goreq.Request{ + Method: "GET", + Uri: server.URL + "/keys/123", + } + request.AddHeader("Authorization", "Bearer "+authToken) + result, err := request.Do() + require.Nil(t, err) + + var response routes.KeysGetResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + require.False(t, response.Success) + require.Equal(t, "Requested key does not exist on our server", response.Message) +} diff --git a/routes/middleware_test.go b/routes/middleware_test.go new file mode 100644 index 0000000..5680885 --- /dev/null +++ b/routes/middleware_test.go @@ -0,0 +1,91 @@ +package routes_test + +import ( + "testing" + "time" + + "github.com/franela/goreq" + "github.com/stretchr/testify/require" + + "github.com/lavab/api/env" + "github.com/lavab/api/models" + "github.com/lavab/api/routes" +) + +func TestMiddlewareNoHeader(t *testing.T) { + result, err := goreq.Request{ + Method: "GET", + Uri: server.URL + "/accounts/me", + }.Do() + require.Nil(t, err) + + var response routes.AuthMiddlewareResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + require.False(t, response.Success) + require.Equal(t, "Missing auth token", response.Message) +} + +func TestMiddlewareInvalidHeader(t *testing.T) { + request := goreq.Request{ + Method: "GET", + Uri: server.URL + "/accounts/me", + } + request.AddHeader("Authorization", "123") + result, err := request.Do() + require.Nil(t, err) + + var response routes.AuthMiddlewareResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + require.False(t, response.Success) + require.Equal(t, "Invalid authorization header", response.Message) +} + +func TestMiddlewareInvalidToken(t *testing.T) { + request := goreq.Request{ + Method: "GET", + Uri: server.URL + "/accounts/me", + } + request.AddHeader("Authorization", "Bearer 123") + result, err := request.Do() + require.Nil(t, err) + + var response routes.AuthMiddlewareResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + require.False(t, response.Success) + require.Equal(t, "Invalid authorization token", response.Message) +} + +func TestMiddlewareExpiredToken(t *testing.T) { + // Prepare a token + token := models.Token{ + Resource: models.MakeResource(accountID, "test invite token"), + Expiring: models.Expiring{ + ExpiryDate: time.Now().UTC().Truncate(time.Hour * 8), + }, + Type: "auth", + } + + err := env.Tokens.Insert(token) + require.Nil(t, err) + + request := goreq.Request{ + Method: "GET", + Uri: server.URL + "/accounts/me", + } + request.AddHeader("Authorization", "Bearer "+token.ID) + result, err := request.Do() + require.Nil(t, err) + + var response routes.AuthMiddlewareResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + require.False(t, response.Success) + require.Equal(t, "Authorization token has expired", response.Message) +} diff --git a/routes/tokens.go b/routes/tokens.go index 54e4ca2..3b11567 100644 --- a/routes/tokens.go +++ b/routes/tokens.go @@ -151,9 +151,9 @@ func TokensDelete(c web.C, w http.ResponseWriter, r *http.Request) { "id": id, }).Warn("Unable to find the token") - utils.JSONResponse(w, 500, &TokensDeleteResponse{ - Success: true, - Message: "Internal server error - TO/DE/01", + utils.JSONResponse(w, 404, &TokensDeleteResponse{ + Success: false, + Message: "Invalid token ID", }) return } @@ -166,7 +166,7 @@ func TokensDelete(c web.C, w http.ResponseWriter, r *http.Request) { }).Error("Unable to delete a token") utils.JSONResponse(w, 500, &TokensDeleteResponse{ - Success: true, + Success: false, Message: "Internal server error - TO/DE/02", }) return diff --git a/routes/tokens_test.go b/routes/tokens_test.go new file mode 100644 index 0000000..6fcf270 --- /dev/null +++ b/routes/tokens_test.go @@ -0,0 +1,263 @@ +package routes_test + +import ( + "testing" + "time" + + "github.com/franela/goreq" + "github.com/stretchr/testify/require" + + "github.com/lavab/api/env" + "github.com/lavab/api/models" + "github.com/lavab/api/routes" +) + +func TestTokensPrepareAccount(t *testing.T) { + const ( + username = "jeremy" + password = "potato" + ) + + // Prepare a token + inviteToken := models.Token{ + Resource: models.MakeResource("", "test invite token"), + Type: "invite", + } + inviteToken.ExpireSoon() + + err := env.Tokens.Insert(inviteToken) + require.Nil(t, err) + + // POST /accounts - invited + result1, err := goreq.Request{ + Method: "POST", + Uri: server.URL + "/accounts", + ContentType: "application/json", + Body: routes.AccountsCreateRequest{ + Username: username, + Password: password, + Token: inviteToken.ID, + }, + }.Do() + require.Nil(t, err) + + // Unmarshal the response + var response1 routes.AccountsCreateResponse + err = result1.Body.FromJsonTo(&response1) + require.Nil(t, err) + + // Check the result's contents + require.True(t, response1.Success) + require.Equal(t, "A new account was successfully created", response1.Message) + require.NotEmpty(t, response1.Account.ID) + + accountID = response1.Account.ID +} + +func TestTokensCreate(t *testing.T) { + // log in as mr jeremy potato + const ( + username = "jeremy" + password = "potato" + ) + // POST /accounts - classic + request, err := goreq.Request{ + Method: "POST", + Uri: server.URL + "/tokens", + ContentType: "application/json", + Body: routes.TokensCreateRequest{ + Username: username, + Password: password, + Type: "auth", + }, + }.Do() + require.Nil(t, err) + + // Unmarshal the response + var response routes.TokensCreateResponse + err = request.Body.FromJsonTo(&response) + require.Nil(t, err) + + // Check the result's contents + require.True(t, response.Success) + require.Equal(t, "Authentication successful", response.Message) + require.NotEmpty(t, response.Token.ID) + + // Populate the global token variable + authToken = response.Token.ID +} + +func TestTokensCreateNonAuth(t *testing.T) { + request, err := goreq.Request{ + Method: "POST", + Uri: server.URL + "/tokens", + ContentType: "application/json", + Body: routes.TokensCreateRequest{ + Type: "not-auth", + }, + }.Do() + require.Nil(t, err) + + var response routes.TokensCreateResponse + err = request.Body.FromJsonTo(&response) + require.Nil(t, err) + + require.False(t, response.Success) + require.Equal(t, "Only auth tokens are implemented", response.Message) +} + +func TestTokensCreateWrongUsername(t *testing.T) { + request, err := goreq.Request{ + Method: "POST", + Uri: server.URL + "/tokens", + ContentType: "application/json", + Body: routes.TokensCreateRequest{ + Type: "auth", + Username: "not-jeremy", + Password: "potato", + }, + }.Do() + require.Nil(t, err) + + var response routes.TokensCreateResponse + err = request.Body.FromJsonTo(&response) + require.Nil(t, err) + + require.False(t, response.Success) + require.Equal(t, "Wrong username or password", response.Message) +} + +func TestTokensCreateWrongPassword(t *testing.T) { + request, err := goreq.Request{ + Method: "POST", + Uri: server.URL + "/tokens", + ContentType: "application/json", + Body: routes.TokensCreateRequest{ + Type: "auth", + Username: "jeremy", + Password: "not-potato", + }, + }.Do() + require.Nil(t, err) + + var response routes.TokensCreateResponse + err = request.Body.FromJsonTo(&response) + require.Nil(t, err) + + require.False(t, response.Success) + require.Equal(t, "Wrong username or password", response.Message) +} + +func TestTokensCreateInvalid(t *testing.T) { + request, err := goreq.Request{ + Method: "POST", + Uri: server.URL + "/tokens", + ContentType: "application/json", + Body: "123123123###434$#$", + }.Do() + require.Nil(t, err) + + // Unmarshal the response + var response routes.TokensCreateResponse + err = request.Body.FromJsonTo(&response) + require.Nil(t, err) + + // Check the result's contents + require.False(t, response.Success) + require.Equal(t, "Invalid input format", response.Message) +} + +func TestTokensGet(t *testing.T) { + request := goreq.Request{ + Method: "GET", + Uri: server.URL + "/tokens", + } + request.AddHeader("Authorization", "Bearer "+authToken) + result, err := request.Do() + require.Nil(t, err) + + var response routes.TokensGetResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + require.True(t, response.Success) + require.True(t, response.Expires.After(time.Now().UTC())) +} + +func TestTokensDeleteById(t *testing.T) { + const ( + username = "jeremy" + password = "potato" + ) + + request1, err := goreq.Request{ + Method: "POST", + Uri: server.URL + "/tokens", + ContentType: "application/json", + Body: routes.TokensCreateRequest{ + Username: username, + Password: password, + Type: "auth", + }, + }.Do() + require.Nil(t, err) + + // Unmarshal the response + var response1 routes.TokensCreateResponse + err = request1.Body.FromJsonTo(&response1) + require.Nil(t, err) + + // Check the result's contents + require.True(t, response1.Success) + require.Equal(t, "Authentication successful", response1.Message) + require.NotEmpty(t, response1.Token.ID) + + request2 := goreq.Request{ + Method: "DELETE", + Uri: server.URL + "/tokens/" + response1.Token.ID, + } + request2.AddHeader("Authorization", "Bearer "+authToken) + result2, err := request2.Do() + require.Nil(t, err) + + var response2 routes.TokensDeleteResponse + err = result2.Body.FromJsonTo(&response2) + require.Nil(t, err) + + require.True(t, response2.Success) + require.Equal(t, "Successfully logged out", response2.Message) +} + +func TestTokensDeleteByInvalidID(t *testing.T) { + request := goreq.Request{ + Method: "DELETE", + Uri: server.URL + "/tokens/123", + } + request.AddHeader("Authorization", "Bearer "+authToken) + result, err := request.Do() + require.Nil(t, err) + + var response routes.TokensDeleteResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + require.False(t, response.Success) + require.Equal(t, "Invalid token ID", response.Message) +} + +func TestTokensDeleteCurrent(t *testing.T) { + request := goreq.Request{ + Method: "DELETE", + Uri: server.URL + "/tokens", + } + request.AddHeader("Authorization", "Bearer "+authToken) + result, err := request.Do() + require.Nil(t, err) + + var response routes.TokensDeleteResponse + err = result.Body.FromJsonTo(&response) + require.Nil(t, err) + + require.True(t, response.Success) + require.Equal(t, "Successfully logged out", response.Message) +} diff --git a/setup/setup.go b/setup/setup.go new file mode 100644 index 0000000..c521f78 --- /dev/null +++ b/setup/setup.go @@ -0,0 +1,163 @@ +package setup + +import ( + "time" + + "github.com/Sirupsen/logrus" + "github.com/dancannon/gorethink" + "github.com/zenazn/goji/web" + "github.com/zenazn/goji/web/middleware" + + "github.com/lavab/api/db" + "github.com/lavab/api/env" + "github.com/lavab/api/routes" + "github.com/lavab/glogrus" +) + +func PrepareMux(flags *env.Flags) *web.Mux { + // Set up a new logger + log := logrus.New() + + // Set the formatter depending on the passed flag's value + if flags.LogFormatterType == "text" { + log.Formatter = &logrus.TextFormatter{ + ForceColors: flags.ForceColors, + } + } else if flags.LogFormatterType == "json" { + log.Formatter = &logrus.JSONFormatter{} + } + + // Pass it to the environment package + env.Log = log + + // Set up the database + rethinkOpts := gorethink.ConnectOpts{ + Address: flags.RethinkDBURL, + AuthKey: flags.RethinkDBKey, + MaxIdle: 10, + IdleTimeout: time.Second * 10, + } + err := db.Setup(rethinkOpts) + if err != nil { + log.WithFields(logrus.Fields{ + "error": err, + }).Fatal("Unable to set up the database") + } + + // Initialize the actual connection + rethinkOpts.Database = flags.RethinkDBDatabase + rethinkSession, err := gorethink.Connect(rethinkOpts) + if err != nil { + log.WithFields(logrus.Fields{ + "error": err, + }).Fatal("Unable to connect to the database") + } + + // Put the RethinkDB session into the environment package + env.Rethink = rethinkSession + + // Initialize the tables + env.Tokens = &db.TokensTable{ + RethinkCRUD: db.NewCRUDTable( + rethinkSession, + rethinkOpts.Database, + "tokens", + ), + } + env.Accounts = &db.AccountsTable{ + RethinkCRUD: db.NewCRUDTable( + rethinkSession, + rethinkOpts.Database, + "accounts", + ), + Tokens: env.Tokens, + } + env.Keys = &db.KeysTable{ + RethinkCRUD: db.NewCRUDTable( + rethinkSession, + rethinkOpts.Database, + "keys", + ), + } + env.Contacts = &db.ContactsTable{ + RethinkCRUD: db.NewCRUDTable( + rethinkSession, + rethinkOpts.Database, + "contacts", + ), + } + + // Create a new goji mux + mux := web.New() + + // Include the most basic middlewares: + // - RequestID assigns an unique ID for each request in order to identify errors. + // - Glogrus logs each request + // - Recoverer prevents panics from crashing the API + // - AutomaticOptions automatically responds to OPTIONS requests + mux.Use(middleware.RequestID) + mux.Use(glogrus.NewGlogrus(log, "api")) + mux.Use(middleware.Recoverer) + mux.Use(middleware.AutomaticOptions) + + // Set up an auth'd mux + auth := web.New() + auth.Use(routes.AuthMiddleware) + + // Index route + mux.Get("/", routes.Hello) + + // Accounts + auth.Get("/accounts", routes.AccountsList) + mux.Post("/accounts", routes.AccountsCreate) + auth.Get("/accounts/:id", routes.AccountsGet) + auth.Put("/accounts/:id", routes.AccountsUpdate) + auth.Delete("/accounts/:id", routes.AccountsDelete) + auth.Post("/accounts/:id/wipe-data", routes.AccountsWipeData) + + // Tokens + auth.Get("/tokens", routes.TokensGet) + mux.Post("/tokens", routes.TokensCreate) + auth.Delete("/tokens", routes.TokensDelete) + auth.Delete("/tokens/:id", routes.TokensDelete) + + // Threads + auth.Get("/threads", routes.ThreadsList) + auth.Get("/threads/:id", routes.ThreadsGet) + auth.Put("/threads/:id", routes.ThreadsUpdate) + + // Emails + auth.Get("/emails", routes.EmailsList) + auth.Post("/emails", routes.EmailsCreate) + auth.Get("/emails/:id", routes.EmailsGet) + auth.Put("/emails/:id", routes.EmailsUpdate) + auth.Delete("/emails/:id", routes.EmailsDelete) + + // Labels + auth.Get("/labels", routes.LabelsList) + auth.Post("/labels", routes.LabelsCreate) + auth.Get("/labels/:id", routes.LabelsGet) + auth.Put("/labels/:id", routes.LabelsUpdate) + auth.Delete("/labels/:id", routes.LabelsDelete) + + // Contacts + auth.Get("/contacts", routes.ContactsList) + auth.Post("/contacts", routes.ContactsCreate) + auth.Get("/contacts/:id", routes.ContactsGet) + auth.Put("/contacts/:id", routes.ContactsUpdate) + auth.Delete("/contacts/:id", routes.ContactsDelete) + + // Keys + mux.Get("/keys", routes.KeysList) + auth.Post("/keys", routes.KeysCreate) + mux.Get("/keys/:id", routes.KeysGet) + auth.Post("/keys/:id/vote", routes.KeysVote) + + // Merge the muxes + mux.Handle("/*", auth) + + // Compile the routes + mux.Compile() + + return mux +} diff --git a/setup/setup_test.go b/setup/setup_test.go new file mode 100644 index 0000000..ebbe49b --- /dev/null +++ b/setup/setup_test.go @@ -0,0 +1,29 @@ +package setup + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/lavab/api/env" +) + +func TestSetup(t *testing.T) { + // Mock data + env.Config = &env.Flags{ + APIVersion: "v0", + LogFormatterType: "text", + ForceColors: true, + + SessionDuration: 72, + ClassicRegistration: true, + + RethinkDBURL: "127.0.0.1:28015", + RethinkDBKey: "", + RethinkDBDatabase: "test", + } + + // Prepare a new mux (initialize the API) + mux := PrepareMux(env.Config) + require.NotNil(t, mux, "mux should not be nil") +}