From 5f93042c100f85489bd9f4bf2cdfacb132ea4298 Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Sun, 30 Nov 2014 12:26:43 +0100 Subject: [PATCH 1/4] Reservation table --- db/table_reservations.go | 6 ++++++ env/env.go | 2 ++ models/reservation.go | 8 ++++++++ setup/setup.go | 8 ++++++++ 4 files changed, 24 insertions(+) create mode 100644 db/table_reservations.go create mode 100644 models/reservation.go diff --git a/db/table_reservations.go b/db/table_reservations.go new file mode 100644 index 0000000..e2c0e57 --- /dev/null +++ b/db/table_reservations.go @@ -0,0 +1,6 @@ +package db + +// ReservationsTable is a CRUD interface for accessing the "reservation" table +type ReservationsTable struct { + RethinkCRUD +} diff --git a/env/env.go b/env/env.go index 18178b6..0a32a7c 100644 --- a/env/env.go +++ b/env/env.go @@ -22,4 +22,6 @@ var ( Keys *db.KeysTable // Contacts is the global instance of ContactsTable Contacts *db.ContactsTable + // Reservations is the global instance of ReservationsTable + Reservations *db.ReservationsTable ) diff --git a/models/reservation.go b/models/reservation.go new file mode 100644 index 0000000..7576cd4 --- /dev/null +++ b/models/reservation.go @@ -0,0 +1,8 @@ +package models + +// Reservation is a closed beta account request +type Reservation struct { + Resource + + Email string +} diff --git a/setup/setup.go b/setup/setup.go index c521f78..ab93a54 100644 --- a/setup/setup.go +++ b/setup/setup.go @@ -14,6 +14,7 @@ import ( "github.com/lavab/glogrus" ) +// PrepareMux sets up the API func PrepareMux(flags *env.Flags) *web.Mux { // Set up a new logger log := logrus.New() @@ -86,6 +87,13 @@ func PrepareMux(flags *env.Flags) *web.Mux { "contacts", ), } + env.Reservations = &db.ReservationsTable{ + RethinkCRUD: db.NewCRUDTable( + rethinkSession, + rethinkOpts.Database, + "reservations", + ), + } // Create a new goji mux mux := web.New() From 37bf18966228b506060159323f6396270ad62c77 Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Wed, 3 Dec 2014 11:52:50 +0100 Subject: [PATCH 2/4] Reservations implementation --- db/default_crud.go | 16 ++++++++++ db/rethink_crud.go | 1 + db/table_accounts.go | 13 ++++++++ db/table_reservations.go | 26 +++++++++++++++ env/config.go | 1 + main.go | 6 ++-- models/reservation.go | 2 +- routes/accounts.go | 69 ++++++++++++++++++++++++++++++++++++---- 8 files changed, 124 insertions(+), 10 deletions(-) diff --git a/db/default_crud.go b/db/default_crud.go index 97f7b9b..c3976c5 100644 --- a/db/default_crud.go +++ b/db/default_crud.go @@ -123,6 +123,22 @@ func (d *Default) FindBy(key string, value interface{}) (*gorethink.Cursor, erro return cursor, nil } +func (d *Default) FindByAndCount(key string, value interface{}) (int, error) { + cursor, err := d.GetTable().Filter(map[string]interface{}{ + key: value, + }).Count().Run(d.session) + if err != nil { + return 0, err + } + + var count int + if err := cursor.One(&count); err != nil { + return 0, NewDatabaseError(d, err, "") + } + + return count, nil +} + // FindByAndFetch retrieves a value by key and then fills results with the result. func (d *Default) FindByAndFetch(key string, value interface{}, results interface{}) error { cursor, err := d.FindBy(key, value) diff --git a/db/rethink_crud.go b/db/rethink_crud.go index b04a381..8c669ef 100644 --- a/db/rethink_crud.go +++ b/db/rethink_crud.go @@ -23,6 +23,7 @@ type RethinkReader interface { FindBy(key string, value interface{}) (*rethink.Cursor, error) FindByAndFetch(key string, value interface{}, results interface{}) error FindByAndFetchOne(key string, value interface{}, result interface{}) error + FindByAndCount(key string, value interface{}) (int, error) Where(filter map[string]interface{}) (*rethink.Cursor, error) WhereAndFetch(filter map[string]interface{}, results interface{}) error diff --git a/db/table_accounts.go b/db/table_accounts.go index 0cc86f1..1bd7dc9 100644 --- a/db/table_accounts.go +++ b/db/table_accounts.go @@ -45,3 +45,16 @@ func (a *AccountsTable) GetTokenOwner(token *models.Token) (*models.Account, err return user, nil } + +func (a *AccountsTable) IsUsernameUsed(name string) (bool, error) { + count, err := a.FindByAndCount("name", name) + if err != nil { + return false, err + } + + if count == 0 { + return false, nil + } + + return true, nil +} diff --git a/db/table_reservations.go b/db/table_reservations.go index e2c0e57..05e14cf 100644 --- a/db/table_reservations.go +++ b/db/table_reservations.go @@ -4,3 +4,29 @@ package db type ReservationsTable struct { RethinkCRUD } + +func (r *ReservationsTable) IsUsernameUsed(name string) (bool, error) { + count, err := r.FindByAndCount("name", name) + if err != nil { + return false, err + } + + if count == 0 { + return false, nil + } + + return true, nil +} + +func (r *ReservationsTable) IsEmailUsed(email string) (bool, error) { + count, err := r.FindByAndCount("email", email) + if err != nil { + return false, err + } + + if count == 0 { + return false, nil + } + + return true, nil +} diff --git a/env/config.go b/env/config.go index 880cb12..93ea52a 100644 --- a/env/config.go +++ b/env/config.go @@ -9,6 +9,7 @@ type Flags struct { SessionDuration int ClassicRegistration bool + UsernameReservation bool RethinkDBURL string RethinkDBKey string diff --git a/main.go b/main.go index 8d20261..db34c48 100644 --- a/main.go +++ b/main.go @@ -22,10 +22,11 @@ var ( bindAddress = flag.String("bind", ":5000", "Network address used to bind") apiVersion = flag.String("version", "v0", "Shown API version") logFormatterType = flag.String("log", "text", "Log formatter type. Either \"json\" or \"text\"") - sessionDuration = flag.Int("session_duration", 72, "Session duration expressed in hours") forceColors = flag.Bool("force_colors", false, "Force colored prompt?") // Registration settings + sessionDuration = flag.Int("session_duration", 72, "Session duration expressed in hours") classicRegistration = flag.Bool("classic_registration", false, "Classic registration enabled?") + usernameReservation = flag.Bool("username_reservation", false, "Username reservation enabled?") // Database-related flags rethinkdbURL = flag.String("rethinkdb_url", func() string { address := os.Getenv("RETHINKDB_PORT_28015_TCP_ADDR") @@ -55,8 +56,9 @@ func main() { LogFormatterType: *logFormatterType, ForceColors: *forceColors, - ClassicRegistration: *classicRegistration, SessionDuration: *sessionDuration, + ClassicRegistration: *classicRegistration, + UsernameReservation: *usernameReservation, RethinkDBURL: *rethinkdbURL, RethinkDBKey: *rethinkdbKey, diff --git a/models/reservation.go b/models/reservation.go index 7576cd4..3f2ed99 100644 --- a/models/reservation.go +++ b/models/reservation.go @@ -4,5 +4,5 @@ package models type Reservation struct { Resource - Email string + Email string `json:"email" gorethink:"email"` } diff --git a/routes/accounts.go b/routes/accounts.go index cb775d7..68e52bd 100644 --- a/routes/accounts.go +++ b/routes/accounts.go @@ -61,13 +61,16 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) { // 1) username + token + password - invite // 2) username + password + alt_email - register with confirmation // 3) alt_email only - register for beta (add to queue) + // 4) alt_email + username - register for beta with username reservation requestType := "unknown" if input.AltEmail == "" && input.Username != "" && input.Password != "" && input.Token != "" { requestType = "invited" } else if input.AltEmail != "" && input.Username != "" && input.Password != "" && input.Token == "" { requestType = "classic" } else if input.AltEmail != "" && input.Username == "" && input.Password == "" && input.Token == "" { - requestType = "queue" + requestType = "queue/classic" + } else if input.AltEmail != "" && input.Username != "" && input.Password == "" && input.Token == "" { + requestType = "queue/reserve" } // "unknown" requests are empty and invalid @@ -79,12 +82,64 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) { return } - // Adding to queue will be implemented soon - if requestType == "queue" { - // Implementation awaits https://trello.com/c/SLM0qK1O/91-account-registration-queue - utils.JSONResponse(w, 501, &AccountsCreateResponse{ - Success: false, - Message: "Sorry, not implemented yet", + // Adding to [beta] queue + if requestType[:5] == "queue" { + if requestType[6:] == "reserve" { + // Is username reservation enabled? + if !env.Config.UsernameReservation { + utils.JSONResponse(w, 403, &AccountsCreateResponse{ + Success: false, + Message: "Username reservation is disabled", + }) + return + } + } + + // Ensure that the email is not already used to reserve/register + if used, err := env.Reservations.IsEmailUsed(input.AltEmail); err != nil || used { + utils.JSONResponse(w, 400, &AccountsCreateResponse{ + Success: false, + Message: "Email already used for a reservation", + }) + return + } + + if requestType[6:] == "reserve" { + if used, err := env.Reservations.IsUsernameUsed(input.Username); err != nil || used { + utils.JSONResponse(w, 400, &AccountsCreateResponse{ + Success: false, + Message: "Username already reserved", + }) + return + } + + if used, err := env.Accounts.IsUsernameUsed(input.Username); err != nil || used { + utils.JSONResponse(w, 400, &AccountsCreateResponse{ + Success: false, + Message: "Username already used", + }) + return + } + } + + // Prepare data to insert + reservation := &models.Reservation{ + Email: input.AltEmail, + Resource: models.MakeResource("", input.Username), + } + + err := env.Reservations.Insert(reservation) + if err != nil { + utils.JSONResponse(w, 500, &AccountsCreateResponse{ + Success: false, + Message: "Internal error while reserving the account", + }) + return + } + + utils.JSONResponse(w, 201, &AccountsCreateResponse{ + Success: true, + Message: "Reserved an account", }) return } From 13142977a18ebfb58e268812f6c878d47031d4c2 Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Fri, 5 Dec 2014 00:09:44 +0100 Subject: [PATCH 3/4] Still bugged --- db/setup.go | 17 ++++---- db/table_accounts.go | 13 ++++++ routes/accounts.go | 28 +++++++----- routes/accounts_test.go | 94 ++++++++++++++++++++++++++++++++++++++++- routes/init_test.go | 1 + 5 files changed, 132 insertions(+), 21 deletions(-) diff --git a/db/setup.go b/db/setup.go index e3fd1f6..51e84c1 100644 --- a/db/setup.go +++ b/db/setup.go @@ -12,14 +12,15 @@ var ( // Indexes of tables in the database var tableIndexes = map[string][]string{ - "tokens": []string{"user", "user_id"}, - "accounts": []string{"name"}, - "emails": []string{"user_id"}, - "drafts": []string{"user_id"}, - "contacts": []string{}, - "threads": []string{"user_id"}, - "labels": []string{}, - "keys": []string{}, + "tokens": []string{"user", "user_id"}, + "accounts": []string{"name"}, + "emails": []string{"user_id"}, + "drafts": []string{"user_id"}, + "contacts": []string{}, + "threads": []string{"user_id"}, + "labels": []string{}, + "keys": []string{}, + "reservations": []string{}, } // List of names of databases diff --git a/db/table_accounts.go b/db/table_accounts.go index 1bd7dc9..4e04931 100644 --- a/db/table_accounts.go +++ b/db/table_accounts.go @@ -58,3 +58,16 @@ func (a *AccountsTable) IsUsernameUsed(name string) (bool, error) { return true, nil } + +func (a *AccountsTable) IsEmailUsed(email string) (bool, error) { + count, err := a.FindByAndCount("alt_email", email) + if err != nil { + return false, err + } + + if count == 0 { + return false, nil + } + + return true, nil +} diff --git a/routes/accounts.go b/routes/accounts.go index 68e52bd..b06c506 100644 --- a/routes/accounts.go +++ b/routes/accounts.go @@ -93,18 +93,7 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) { }) return } - } - - // Ensure that the email is not already used to reserve/register - if used, err := env.Reservations.IsEmailUsed(input.AltEmail); err != nil || used { - utils.JSONResponse(w, 400, &AccountsCreateResponse{ - Success: false, - Message: "Email already used for a reservation", - }) - return - } - if requestType[6:] == "reserve" { if used, err := env.Reservations.IsUsernameUsed(input.Username); err != nil || used { utils.JSONResponse(w, 400, &AccountsCreateResponse{ Success: false, @@ -122,6 +111,23 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) { } } + // Ensure that the email is not already used to reserve/register + if used, err := env.Reservations.IsEmailUsed(input.AltEmail); err != nil || used { + utils.JSONResponse(w, 400, &AccountsCreateResponse{ + Success: false, + Message: "Email already used for a reservation", + }) + return + } + + if used, err := env.Accounts.IsEmailUsed(input.AltEmail); err != nil || used { + utils.JSONResponse(w, 400, &AccountsCreateResponse{ + Success: false, + Message: "Email already used for a reservation", + }) + return + } + // Prepare data to insert reservation := &models.Reservation{ Email: input.AltEmail, diff --git a/routes/accounts_test.go b/routes/accounts_test.go index 6c07c8f..5bb751f 100644 --- a/routes/accounts_test.go +++ b/routes/accounts_test.go @@ -291,7 +291,97 @@ func TestAccountsCreateClassicDisabled(t *testing.T) { env.Config.ClassicRegistration = true } -func TestAccountsCreateQueue(t *testing.T) { +func TestAccountsCreateQueueReservation(t *testing.T) { + result, err := goreq.Request{ + Method: "POST", + Uri: server.URL + "/accounts", + ContentType: "application/json", + Body: routes.AccountsCreateRequest{ + AltEmail: "reserved@example.com", + Username: "reserved", + }, + }.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.Equal(t, "Reserved an account", response.Message) + require.True(t, response.Success) +} + +func TestAccountsCreateQueueReservationUsernameReserved(t *testing.T) { + result, err := goreq.Request{ + Method: "POST", + Uri: server.URL + "/accounts", + ContentType: "application/json", + Body: routes.AccountsCreateRequest{ + AltEmail: "not-reserved@example.com", + Username: "reserved", + }, + }.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.Equal(t, "Username already reserved", response.Message) + require.False(t, response.Success) +} + +func TestAccountsCreateQueueReservationUsernameTaken(t *testing.T) { + result, err := goreq.Request{ + Method: "POST", + Uri: server.URL + "/accounts", + ContentType: "application/json", + Body: routes.AccountsCreateRequest{ + AltEmail: "not-reserved@example.com", + Username: "jeremy", + }, + }.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 used", response.Message) +} + +func TestAccountsCreateQueueReservationDisabled(t *testing.T) { + env.Config.UsernameReservation = false + result, err := goreq.Request{ + Method: "POST", + Uri: server.URL + "/accounts", + ContentType: "application/json", + Body: routes.AccountsCreateRequest{ + AltEmail: "something@example.com", + Username: "something", + }, + }.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 reservation is disabled", response.Message) + env.Config.UsernameReservation = true +} + +func TestAccountsCreateQueueClassicUsedEmail(t *testing.T) { // POST /accounts - queue result, err := goreq.Request{ Method: "POST", @@ -309,8 +399,8 @@ func TestAccountsCreateQueue(t *testing.T) { require.Nil(t, err) // Check the result's contents + require.Equal(t, "Email already used for a reservation", response.Message) require.False(t, response.Success) - require.Equal(t, "Sorry, not implemented yet", response.Message) } func TestAccountsPrepareToken(t *testing.T) { diff --git a/routes/init_test.go b/routes/init_test.go index 3c1ffb4..6d1d576 100644 --- a/routes/init_test.go +++ b/routes/init_test.go @@ -25,6 +25,7 @@ func init() { SessionDuration: 72, ClassicRegistration: true, + UsernameReservation: true, RethinkDBURL: "127.0.0.1:28015", RethinkDBKey: "", From ae998cbd451f99b4c5608c4fd67077b680775af5 Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Fri, 5 Dec 2014 00:33:16 +0100 Subject: [PATCH 4/4] Fixed tests --- routes/accounts.go | 1 + routes/accounts_test.go | 25 +++++++++++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/routes/accounts.go b/routes/accounts.go index b06c506..282a862 100644 --- a/routes/accounts.go +++ b/routes/accounts.go @@ -209,6 +209,7 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) { account := &models.Account{ Resource: models.MakeResource("", input.Username), Type: "beta", + AltEmail: input.AltEmail, } // Set the password diff --git a/routes/accounts_test.go b/routes/accounts_test.go index 5bb751f..6b2f61c 100644 --- a/routes/accounts_test.go +++ b/routes/accounts_test.go @@ -23,7 +23,6 @@ func TestAccountsCreateInvalid(t *testing.T) { 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) @@ -230,7 +229,7 @@ func TestAccountsCreateInvitedWrongType(t *testing.T) { func TestAccountsCreateClassic(t *testing.T) { const ( - username = "jeremy_was_invited" + username = "jeremy" password = "potato" ) @@ -403,6 +402,28 @@ func TestAccountsCreateQueueClassicUsedEmail(t *testing.T) { require.False(t, response.Success) } +func TestAccountsCreateQueueClassicReservedEmail(t *testing.T) { + // POST /accounts - queue + result, err := goreq.Request{ + Method: "POST", + Uri: server.URL + "/accounts", + ContentType: "application/json", + Body: routes.AccountsCreateRequest{ + AltEmail: "reserved@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.Equal(t, "Email already used for a reservation", response.Message) + require.False(t, response.Success) +} + func TestAccountsPrepareToken(t *testing.T) { // log in as mr jeremy potato const (