From 07a25f8a31319ed4145fe69f3fd2edf0120fe7da Mon Sep 17 00:00:00 2001 From: G Date: Sat, 15 Sep 2018 18:54:13 -0700 Subject: [PATCH 01/20] refactor into Env/datastore format --- databaseutil/databaseutil.go | 191 ---------------------------- handlers/handlers.go | 28 ++-- main.go | 16 ++- models/category.go | 22 ++++ models/datastore.go | 55 ++++++++ models/note.go | 44 +++++++ models/notemap.go | 19 +++ models/user.go | 114 ++++++++++++++++- services/noteservice/noteservice.go | 48 ------- services/userservice/userservice.go | 92 -------------- 10 files changed, 285 insertions(+), 344 deletions(-) delete mode 100644 databaseutil/databaseutil.go create mode 100644 models/category.go create mode 100644 models/datastore.go create mode 100644 models/notemap.go delete mode 100644 services/noteservice/noteservice.go delete mode 100644 services/userservice/userservice.go diff --git a/databaseutil/databaseutil.go b/databaseutil/databaseutil.go deleted file mode 100644 index 02e24cf..0000000 --- a/databaseutil/databaseutil.go +++ /dev/null @@ -1,191 +0,0 @@ -/* -Package databaseutil abstracts away details about sql and postgres. - -These functions only accept and return primitive types. -*/ -package databaseutil - -import ( - "database/sql" - "errors" - "time" - - "github.com/lib/pq" -) - -var db *sql.DB - -// UniqueConstraintError is returned when a uniqueness constraint is violated during an insert. -var UniqueConstraintError = errors.New("postgres: unique constraint violation") - -// QueryResultContainedMultipleRowsError is returned when a query unexpectedly returns more than one row. -var QueryResultContainedMultipleRowsError = errors.New("query result unexpectedly contained multiple rows") - -// QueryResultContainedNoRowsError is returned when a query unexpectedly returns no rows. -var QueryResultContainedNoRowsError = errors.New("query result unexpectedly contained no rows") - -// ConnectToDatabase also pings the database to ensure a working connection. -func ConnectToDatabase(databaseUrl string) error { - { - tempDb, err := sql.Open("postgres", databaseUrl) - if err != nil { - return err - } - - db = tempDb - } - - if err := db.Ping(); err != nil { - return err - } - - return nil -} - -func InsertIntoUserTable( - displayName string, - emailAddress string, - password []byte, - creationTime time.Time, -) error { - sqlQuery := ` - INSERT INTO app_user (display_name, email_address, password, creation_time) - VALUES ($1, $2, $3, $4)` - - rows, err := db.Query(sqlQuery, displayName, emailAddress, password, creationTime) - if err != nil { - return convertPostgresError(err) - } - defer rows.Close() - - if err := rows.Err(); err != nil { - return convertPostgresError(err) - } - - return nil -} - -func GetPasswordForUserWithEmailAddress(emailAddress string) ([]byte, error) { - sqlQuery := ` - SELECT password FROM app_user - WHERE email_address = $1` - - rows, err := db.Query(sqlQuery, emailAddress) - if err != nil { - return nil, convertPostgresError(err) - } - defer rows.Close() - - var password []byte - for rows.Next() { - if password != nil { - return nil, QueryResultContainedMultipleRowsError - } - - if err := rows.Scan(&password); err != nil { - return nil, err - } - } - - if password == nil { - return nil, QueryResultContainedNoRowsError - } - - return password, nil -} - -func InsertNewNote(authorId int64, content string, creationTime time.Time) (int64, error) { - sqlQuery := ` - INSERT INTO note (author_id, content, creation_time) - VALUES ($1, $2, $3) - RETURNING id` - - rows, err := db.Query(sqlQuery, authorId, content, creationTime) - if err != nil { - return 0, convertPostgresError(err) - } - defer rows.Close() - - var noteId int64 = 0 - for rows.Next() { - - if noteId != 0 { - return 0, QueryResultContainedMultipleRowsError - } - - if err := rows.Scan(¬eId); err != nil { - return 0, convertPostgresError(err) - } - } - - if noteId == 0 { - return 0, QueryResultContainedNoRowsError - } - - if err := rows.Err(); err != nil { - return 0, convertPostgresError(err) - } - - return noteId, nil -} - -func InsertNoteCategoryRelationship(noteId int64, category string) error { - sqlQuery := ` - INSERT INTO note_to_category_relationship (note_id, category) - VALUES ($1, $2)` - - rows, err := db.Query(sqlQuery, noteId, category) - if err != nil { - return convertPostgresError(err) - } - defer rows.Close() - - if err := rows.Err(); err != nil { - return convertPostgresError(err) - } - - return nil -} - -func GetIdForUserWithEmailAddress(emailAddress string) (int64, error) { - sqlQuery := ` - SELECT id FROM app_user - WHERE email_address = $1` - - rows, err := db.Query(sqlQuery, emailAddress) - if err != nil { - return 0, convertPostgresError(err) - } - defer rows.Close() - - var userId int64 - for rows.Next() { - if userId != 0 { - return 0, QueryResultContainedMultipleRowsError - } - - if err := rows.Scan(&userId); err != nil { - return 0, err - } - } - - if userId == 0 { - return 0, QueryResultContainedNoRowsError - } - - return userId, nil -} - -// PRIVATE - -func convertPostgresError(err error) error { - const uniqueConstraintErrorCode = "23505" - - if postgresErr, ok := err.(*pq.Error); ok { - if postgresErr.Code == uniqueConstraintErrorCode { - return UniqueConstraintError - } - } - - return err -} diff --git a/handlers/handlers.go b/handlers/handlers.go index dd1a769..3b4cfd1 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -11,8 +11,6 @@ import ( "github.com/atmiguel/cerealnotes/models" "github.com/atmiguel/cerealnotes/paths" - "github.com/atmiguel/cerealnotes/services/noteservice" - "github.com/atmiguel/cerealnotes/services/userservice" "github.com/dgrijalva/jwt-go" ) @@ -28,12 +26,22 @@ type JwtTokenClaim struct { jwt.StandardClaims } +type Environment struct { + Db models.Datastore +} + var tokenSigningKey []byte func SetTokenSigningKey(key []byte) { tokenSigningKey = key } +var environment *Environment + +func SetEnvironment(env *Environment) { + environment = env +} + // UNAUTHENTICATED HANDLERS // HandleLoginOrSignupPageRequest responds to unauthenticated GET requests with the login or signup page. @@ -86,12 +94,12 @@ func HandleUserApiRequest( } var statusCode int - if err := userservice.StoreNewUser( + if err := environment.Db.StoreNewUser( signupForm.DisplayName, models.NewEmailAddress(signupForm.EmailAddress), signupForm.Password, ); err != nil { - if err == userservice.EmailAddressAlreadyInUseError { + if err == models.EmailAddressAlreadyInUseError { statusCode = http.StatusConflict } else { http.Error(responseWriter, err.Error(), http.StatusInternalServerError) @@ -153,12 +161,12 @@ func HandleSessionApiRequest( return } - if err := userservice.AuthenticateUserCredentials( + if err := environment.Db.AuthenticateUserCredentials( models.NewEmailAddress(loginForm.EmailAddress), loginForm.Password, ); err != nil { statusCode := http.StatusInternalServerError - if err == userservice.CredentialsNotAuthorizedError { + if err == models.CredentialsNotAuthorizedError { statusCode = http.StatusUnauthorized } http.Error(responseWriter, err.Error(), statusCode) @@ -167,7 +175,7 @@ func HandleSessionApiRequest( // Set our cookie to have a valid JWT Token as the value { - userId, err := userservice.GetIdForUserWithEmailAddress(models.NewEmailAddress(loginForm.EmailAddress)) + userId, err := environment.Db.GetIdForUserWithEmailAddress(models.NewEmailAddress(loginForm.EmailAddress)) if err != nil { http.Error(responseWriter, err.Error(), http.StatusInternalServerError) return @@ -224,7 +232,7 @@ func HandleNoteApiRequest( switch request.Method { case http.MethodGet: - var notesById noteservice.NoteMap = make(map[models.NoteId]*models.Note, 2) + var notesById models.NoteMap = make(map[models.NoteId]*models.Note, 2) notesById[models.NoteId(1)] = &models.Note{ AuthorId: 1, @@ -272,7 +280,7 @@ func HandleNoteApiRequest( CreationTime: time.Now().UTC(), } - noteId, err := noteservice.StoreNewNote(note) + noteId, err := environment.Db.StoreNewNote(note) if err != nil { http.Error(responseWriter, err.Error(), http.StatusInternalServerError) return @@ -313,7 +321,7 @@ func HandleCategoryApiRequest( return } - if err := noteservice.StoreNewNoteCategoryRelationship(models.NoteId(noteForm.NoteId), category); err != nil { + if err := environment.Db.StoreNewNoteCategoryRelationship(models.NoteId(noteForm.NoteId), category); err != nil { http.Error(responseWriter, err.Error(), http.StatusInternalServerError) return } diff --git a/main.go b/main.go index 60fb3ab..32c31dc 100644 --- a/main.go +++ b/main.go @@ -6,8 +6,8 @@ import ( "net/http" "os" - "github.com/atmiguel/cerealnotes/databaseutil" "github.com/atmiguel/cerealnotes/handlers" + "github.com/atmiguel/cerealnotes/models" "github.com/atmiguel/cerealnotes/routers" ) @@ -53,15 +53,23 @@ func determineTokenSigningKey() ([]byte, error) { func main() { // Set up db + + env := &handlers.Environment{} + { databaseUrl, err := determineDatabaseUrl() if err != nil { log.Fatal(err) } - if err := databaseutil.ConnectToDatabase(databaseUrl); err != nil { + db, err := models.ConnectToDatabase(databaseUrl) + + if err != nil { log.Fatal(err) } + + env.Db = db + } // Set up token signing key @@ -74,6 +82,10 @@ func main() { handlers.SetTokenSigningKey(tokenSigningKey) } + { + handlers.SetEnvironment(env) + } + // Start server { port, err := determineListenPort() diff --git a/models/category.go b/models/category.go new file mode 100644 index 0000000..c8b2571 --- /dev/null +++ b/models/category.go @@ -0,0 +1,22 @@ +package models + +func (db *DB) StoreNewNoteCategoryRelationship( + noteId NoteId, + category Category, +) error { + sqlQuery := ` + INSERT INTO note_to_category_relationship (note_id, category) + VALUES ($1, $2)` + + rows, err := db.Query(sqlQuery, int64(noteId), category.String()) + if err != nil { + return convertPostgresError(err) + } + defer rows.Close() + + if err := rows.Err(); err != nil { + return convertPostgresError(err) + } + + return nil +} diff --git a/models/datastore.go b/models/datastore.go new file mode 100644 index 0000000..88d7a4b --- /dev/null +++ b/models/datastore.go @@ -0,0 +1,55 @@ +package models + +import ( + "database/sql" + "errors" + + "github.com/lib/pq" +) + +// UniqueConstraintError is returned when a uniqueness constraint is violated during an insert. +var UniqueConstraintError = errors.New("postgres: unique constraint violation") + +// QueryResultContainedMultipleRowsError is returned when a query unexpectedly returns more than one row. +var QueryResultContainedMultipleRowsError = errors.New("query result unexpectedly contained multiple rows") + +// QueryResultContainedNoRowsError is returned when a query unexpectedly returns no rows. +var QueryResultContainedNoRowsError = errors.New("query result unexpectedly contained no rows") + +// ConnectToDatabase also pings the database to ensure a working connection. +func ConnectToDatabase(databaseUrl string) (*DB, error) { + tempDb, err := sql.Open("postgres", databaseUrl) + if err != nil { + return nil, err + } + + if err := tempDb.Ping(); err != nil { + return nil, err + } + + return &DB{tempDb}, nil +} + +type Datastore interface { + StoreNewNote(*Note) (NoteId, error) + StoreNewNoteCategoryRelationship(NoteId, Category) error + StoreNewUser(string, *EmailAddress, string) error + AuthenticateUserCredentials(*EmailAddress, string) error + GetIdForUserWithEmailAddress(*EmailAddress) (UserId, error) +} + +type DB struct { + *sql.DB +} + +func convertPostgresError(err error) error { + const uniqueConstraintErrorCode = "23505" + + if postgresErr, ok := err.(*pq.Error); ok { + if postgresErr.Code == uniqueConstraintErrorCode { + return UniqueConstraintError + } + } + + return err +} diff --git a/models/note.go b/models/note.go index 8685a23..abed956 100644 --- a/models/note.go +++ b/models/note.go @@ -48,3 +48,47 @@ type Note struct { Content string `json:"content"` CreationTime time.Time `json:"creationTime"` } + +// DB methods + +func (db *DB) StoreNewNote( + note *Note, +) (NoteId, error) { + + authorId := int64(note.AuthorId) + content := note.Content + creationTime := note.CreationTime + + sqlQuery := ` + INSERT INTO note (author_id, content, creation_time) + VALUES ($1, $2, $3) + RETURNING id` + + rows, err := db.Query(sqlQuery, authorId, content, creationTime) + if err != nil { + return 0, convertPostgresError(err) + } + defer rows.Close() + + var noteId int64 = 0 + for rows.Next() { + + if noteId != 0 { + return 0, QueryResultContainedMultipleRowsError + } + + if err := rows.Scan(¬eId); err != nil { + return 0, convertPostgresError(err) + } + } + + if noteId == 0 { + return 0, QueryResultContainedNoRowsError + } + + if err := rows.Err(); err != nil { + return 0, convertPostgresError(err) + } + + return NoteId(noteId), nil +} diff --git a/models/notemap.go b/models/notemap.go new file mode 100644 index 0000000..1913baa --- /dev/null +++ b/models/notemap.go @@ -0,0 +1,19 @@ +package models + +import ( + "encoding/json" + "fmt" +) + +type NoteMap map[NoteId]*Note + +func (noteMap NoteMap) ToJson() ([]byte, error) { + // json doesn't support int indexed maps + notesByIdString := make(map[string]Note, len(noteMap)) + + for id, note := range noteMap { + notesByIdString[fmt.Sprint(id)] = *note + } + + return json.Marshal(notesByIdString) +} diff --git a/models/user.go b/models/user.go index 0ecac0c..c0b2a31 100644 --- a/models/user.go +++ b/models/user.go @@ -1,6 +1,12 @@ package models -import "strings" +import ( + "errors" + "strings" + "time" + + "golang.org/x/crypto/bcrypt" +) type UserId int64 @@ -20,3 +26,109 @@ func NewEmailAddress(emailAddressAsString string) *EmailAddress { func (emailAddress *EmailAddress) String() string { return emailAddress.emailAddressAsString } + +var EmailAddressAlreadyInUseError = errors.New("Email address already in use") + +var CredentialsNotAuthorizedError = errors.New("The provided credentials were not found") + +// + +func (db *DB) StoreNewUser( + displayName string, + emailAddress *EmailAddress, + password string, +) error { + hashedPassword, err := bcrypt.GenerateFromPassword( + []byte(password), + bcrypt.DefaultCost) + if err != nil { + return err + } + + creationTime := time.Now().UTC() + + sqlQuery := ` + INSERT INTO app_user (display_name, email_address, password, creation_time) + VALUES ($1, $2, $3, $4)` + + rows, err := db.Query(sqlQuery, displayName, emailAddress.String(), hashedPassword, creationTime) + if err != nil { + return convertPostgresError(err) + } + defer rows.Close() + + if err := rows.Err(); err != nil { + return convertPostgresError(err) + } + + return nil +} + +func (db *DB) AuthenticateUserCredentials(emailAddress *EmailAddress, password string) error { + sqlQuery := ` + SELECT password FROM app_user + WHERE email_address = $1` + + rows, err := db.Query(sqlQuery, emailAddress) + if err != nil { + return convertPostgresError(err) + } + defer rows.Close() + + var storedHashedPassword []byte + for rows.Next() { + if storedHashedPassword != nil { + return QueryResultContainedMultipleRowsError + } + + if err := rows.Scan(&storedHashedPassword); err != nil { + return err + } + } + + if storedHashedPassword == nil { + return QueryResultContainedNoRowsError + } + + if err := bcrypt.CompareHashAndPassword( + storedHashedPassword, + []byte(password), + ); err != nil { + if err == bcrypt.ErrMismatchedHashAndPassword { + return CredentialsNotAuthorizedError + } + + return err + } + + return nil +} + +func (db *DB) GetIdForUserWithEmailAddress(emailAddress *EmailAddress) (UserId, error) { + sqlQuery := ` + SELECT id FROM app_user + WHERE email_address = $1` + + rows, err := db.Query(sqlQuery, emailAddress.String()) + if err != nil { + return 0, convertPostgresError(err) + } + defer rows.Close() + + var userId int64 + for rows.Next() { + if userId != 0 { + return 0, QueryResultContainedMultipleRowsError + } + + if err := rows.Scan(&userId); err != nil { + return 0, err + } + } + + if userId == 0 { + return 0, QueryResultContainedNoRowsError + } + + return UserId(userId), nil +} diff --git a/services/noteservice/noteservice.go b/services/noteservice/noteservice.go deleted file mode 100644 index 52a9b77..0000000 --- a/services/noteservice/noteservice.go +++ /dev/null @@ -1,48 +0,0 @@ -/* -Package noteservice handles interactions with database layer. -*/ -package noteservice - -import ( - "encoding/json" - "fmt" - - "github.com/atmiguel/cerealnotes/databaseutil" - "github.com/atmiguel/cerealnotes/models" -) - -func StoreNewNote( - note *models.Note, -) (models.NoteId, error) { - - id, err := databaseutil.InsertNewNote(int64(note.AuthorId), note.Content, note.CreationTime) - if err != nil { - return models.NoteId(0), err - } - - return models.NoteId(id), nil -} - -func StoreNewNoteCategoryRelationship( - noteId models.NoteId, - category models.Category, -) error { - if err := databaseutil.InsertNoteCategoryRelationship(int64(noteId), category.String()); err != nil { - return err - } - - return nil -} - -type NoteMap map[models.NoteId]*models.Note - -func (noteMap NoteMap) ToJson() ([]byte, error) { - // json doesn't support int indexed maps - notesByIdString := make(map[string]models.Note, len(noteMap)) - - for id, note := range noteMap { - notesByIdString[fmt.Sprint(id)] = *note - } - - return json.Marshal(notesByIdString) -} diff --git a/services/userservice/userservice.go b/services/userservice/userservice.go deleted file mode 100644 index fab7356..0000000 --- a/services/userservice/userservice.go +++ /dev/null @@ -1,92 +0,0 @@ -/* -Package userservice handles interactions with database layer. -*/ -package userservice - -import ( - "errors" - "time" - - "github.com/atmiguel/cerealnotes/databaseutil" - "github.com/atmiguel/cerealnotes/models" - "golang.org/x/crypto/bcrypt" -) - -var EmailAddressAlreadyInUseError = errors.New("Email address already in use") - -var CredentialsNotAuthorizedError = errors.New("The provided credentials were not found") - -func StoreNewUser( - displayName string, - emailAddress *models.EmailAddress, - password string, -) error { - hashedPassword, err := bcrypt.GenerateFromPassword( - []byte(password), - bcrypt.DefaultCost) - if err != nil { - return err - } - - creationTime := time.Now().UTC() - - if err := databaseutil.InsertIntoUserTable( - displayName, - emailAddress.String(), - hashedPassword, - creationTime, - ); err != nil { - if err == databaseutil.UniqueConstraintError { - return EmailAddressAlreadyInUseError - } - - return err - } - - return nil -} - -func AuthenticateUserCredentials(emailAddress *models.EmailAddress, password string) error { - storedHashedPassword, err := databaseutil.GetPasswordForUserWithEmailAddress(emailAddress.String()) - if err != nil { - if err == databaseutil.QueryResultContainedMultipleRowsError { - return err // would normally throw a runtime here - } - - if err == databaseutil.QueryResultContainedNoRowsError { - return CredentialsNotAuthorizedError - } - - return err - } - - if err := bcrypt.CompareHashAndPassword( - storedHashedPassword, - []byte(password), - ); err != nil { - if err == bcrypt.ErrMismatchedHashAndPassword { - return CredentialsNotAuthorizedError - } - - return err - } - - return nil -} - -func GetIdForUserWithEmailAddress(emailAddress *models.EmailAddress) (models.UserId, error) { - userIdAsInt, err := databaseutil.GetIdForUserWithEmailAddress(emailAddress.String()) - if err != nil { - if err == databaseutil.QueryResultContainedMultipleRowsError { - return 0, err // would normally throw a runtime here - } - - if err == databaseutil.QueryResultContainedNoRowsError { - return 0, err - } - - return 0, err - } - - return models.UserId(userIdAsInt), nil -} From 26c279037632c38b44772f4487df46e57e241bb9 Mon Sep 17 00:00:00 2001 From: G Date: Sat, 15 Sep 2018 18:58:01 -0700 Subject: [PATCH 02/20] fixed small type issue --- models/user.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/user.go b/models/user.go index c0b2a31..b90198f 100644 --- a/models/user.go +++ b/models/user.go @@ -69,7 +69,7 @@ func (db *DB) AuthenticateUserCredentials(emailAddress *EmailAddress, password s SELECT password FROM app_user WHERE email_address = $1` - rows, err := db.Query(sqlQuery, emailAddress) + rows, err := db.Query(sqlQuery, emailAddress.String()) if err != nil { return convertPostgresError(err) } From 23e528c626cabfa2adfc7bb0d295a5c322a4635d Mon Sep 17 00:00:00 2001 From: G Date: Sat, 15 Sep 2018 21:41:49 -0700 Subject: [PATCH 03/20] login test --- integration_test.go | 129 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 integration_test.go diff --git a/integration_test.go b/integration_test.go new file mode 100644 index 0000000..0fd1689 --- /dev/null +++ b/integration_test.go @@ -0,0 +1,129 @@ +package main_test + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "path/filepath" + "reflect" + "runtime" + "testing" + + "github.com/atmiguel/cerealnotes/handlers" + "github.com/atmiguel/cerealnotes/models" + "github.com/atmiguel/cerealnotes/paths" + "github.com/atmiguel/cerealnotes/routers" +) + +func TestLoginOrSignUpPage(t *testing.T) { + mockDb := &DiyMockDataStore{} + env := &handlers.Environment{mockDb} + + handlers.SetEnvironment(env) + handlers.SetTokenSigningKey([]byte("")) + + server := httptest.NewServer(routers.DefineRoutes()) + defer server.Close() + + resp, err := http.Get(server.URL) + ok(t, err) + + // fmt.Println(ioutil.ReadAll(resp.Body)) + equals(t, 200, resp.StatusCode) +} + +func TestLoginApi(t *testing.T) { + mockDb := &DiyMockDataStore{} + env := &handlers.Environment{mockDb} + + handlers.SetEnvironment(env) + handlers.SetTokenSigningKey([]byte("")) + + server := httptest.NewServer(routers.DefineRoutes()) + defer server.Close() + + theEmail := "justsomeemail@gmail.com" + thePassword := "worldsBestPassword" + + mockDb.Func_AuthenticateUserCredentials = func(email *models.EmailAddress, password string) error { + if email.String() == theEmail && password == thePassword { + return nil + } + + return models.CredentialsNotAuthorizedError + } + + mockDb.Func_GetIdForUserWithEmailAddress = func(email *models.EmailAddress) (models.UserId, error) { + return models.UserId(1), nil + } + + values := map[string]string{"emailAddress": theEmail, "password": thePassword} + + jsonValue, _ := json.Marshal(values) + + resp, err := http.Post(server.URL+paths.SessionApi, "application/json", bytes.NewBuffer(jsonValue)) + + ok(t, err) + + // fmt.Println(ioutil.ReadAll(resp.Body)) + equals(t, 201, resp.StatusCode) +} + +// Helpers + +type DiyMockDataStore struct { + Func_StoreNewNote func(*models.Note) (models.NoteId, error) + Func_StoreNewNoteCategoryRelationship func(models.NoteId, models.Category) error + Func_StoreNewUser func(string, *models.EmailAddress, string) error + Func_AuthenticateUserCredentials func(*models.EmailAddress, string) error + Func_GetIdForUserWithEmailAddress func(*models.EmailAddress) (models.UserId, error) +} + +func (mock *DiyMockDataStore) StoreNewNote(note *models.Note) (models.NoteId, error) { + return mock.Func_StoreNewNote(note) +} + +func (mock *DiyMockDataStore) StoreNewNoteCategoryRelationship(noteId models.NoteId, cat models.Category) error { + return mock.Func_StoreNewNoteCategoryRelationship(noteId, cat) +} + +func (mock *DiyMockDataStore) StoreNewUser(str1 string, email *models.EmailAddress, str2 string) error { + return mock.Func_StoreNewUser(str1, email, str2) +} + +func (mock *DiyMockDataStore) AuthenticateUserCredentials(email *models.EmailAddress, str string) error { + return mock.Func_AuthenticateUserCredentials(email, str) +} + +func (mock *DiyMockDataStore) GetIdForUserWithEmailAddress(email *models.EmailAddress) (models.UserId, error) { + return mock.Func_GetIdForUserWithEmailAddress(email) +} + +// assert fails the test if the condition is false. +func assert(tb testing.TB, condition bool, msg string, v ...interface{}) { + if !condition { + _, file, line, _ := runtime.Caller(1) + fmt.Printf("\033[31m%s:%d: "+msg+"\033[39m\n\n", append([]interface{}{filepath.Base(file), line}, v...)...) + tb.FailNow() + } +} + +// ok fails the test if an err is not nil. +func ok(tb testing.TB, err error) { + if err != nil { + _, file, line, _ := runtime.Caller(1) + fmt.Printf("\033[31m%s:%d: unexpected error: %s\033[39m\n\n", filepath.Base(file), line, err.Error()) + tb.FailNow() + } +} + +// equals fails the test if exp is not equal to act. +func equals(tb testing.TB, exp, act interface{}) { + if !reflect.DeepEqual(exp, act) { + _, file, line, _ := runtime.Caller(1) + fmt.Printf("\033[31m%s:%d:\n\n\texp: %#v\n\n\tgot: %#v\033[39m\n\n", filepath.Base(file), line, exp, act) + tb.FailNow() + } +} From e74e4ff756b4f7f14337dea7ca3cf22caafbcaf7 Mon Sep 17 00:00:00 2001 From: Giuseppe Date: Sun, 16 Sep 2018 22:32:04 -0700 Subject: [PATCH 04/20] removing global variables --- handlers/handlers.go | 53 ++++++++++++++++----------------------- handlers/tokenutil.go | 58 +++++++++++++++++++++---------------------- integration_test.go | 14 +++-------- main.go | 9 ++----- routers/routers.go | 23 +++++++++-------- 5 files changed, 67 insertions(+), 90 deletions(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index 3b4cfd1..bc5f6f8 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -27,32 +27,21 @@ type JwtTokenClaim struct { } type Environment struct { - Db models.Datastore -} - -var tokenSigningKey []byte - -func SetTokenSigningKey(key []byte) { - tokenSigningKey = key -} - -var environment *Environment - -func SetEnvironment(env *Environment) { - environment = env + Db models.Datastore + TokenSigningKey []byte } // UNAUTHENTICATED HANDLERS // HandleLoginOrSignupPageRequest responds to unauthenticated GET requests with the login or signup page. // For authenticated requests, it redirects to the home page. -func HandleLoginOrSignupPageRequest( +func (env *Environment) HandleLoginOrSignupPageRequest( responseWriter http.ResponseWriter, request *http.Request, ) { switch request.Method { case http.MethodGet: - if _, err := getUserIdFromJwtToken(request); err == nil { + if _, err := env.getUserIdFromJwtToken(request); err == nil { http.Redirect( responseWriter, request, @@ -74,7 +63,7 @@ func HandleLoginOrSignupPageRequest( } } -func HandleUserApiRequest( +func (env *Environment) HandleUserApiRequest( responseWriter http.ResponseWriter, request *http.Request, ) { @@ -94,7 +83,7 @@ func HandleUserApiRequest( } var statusCode int - if err := environment.Db.StoreNewUser( + if err := env.Db.StoreNewUser( signupForm.DisplayName, models.NewEmailAddress(signupForm.EmailAddress), signupForm.Password, @@ -113,7 +102,7 @@ func HandleUserApiRequest( case http.MethodGet: - if _, err := getUserIdFromJwtToken(request); err != nil { + if _, err := env.getUserIdFromJwtToken(request); err != nil { http.Error(responseWriter, err.Error(), http.StatusUnauthorized) return } @@ -143,7 +132,7 @@ func HandleUserApiRequest( // HandleSessionApiRequest responds to POST requests by authenticating and responding with a JWT. // It responds to DELETE requests by expiring the client's cookie. -func HandleSessionApiRequest( +func (env *Environment) HandleSessionApiRequest( responseWriter http.ResponseWriter, request *http.Request, ) { @@ -161,7 +150,7 @@ func HandleSessionApiRequest( return } - if err := environment.Db.AuthenticateUserCredentials( + if err := env.Db.AuthenticateUserCredentials( models.NewEmailAddress(loginForm.EmailAddress), loginForm.Password, ); err != nil { @@ -175,13 +164,13 @@ func HandleSessionApiRequest( // Set our cookie to have a valid JWT Token as the value { - userId, err := environment.Db.GetIdForUserWithEmailAddress(models.NewEmailAddress(loginForm.EmailAddress)) + userId, err := env.Db.GetIdForUserWithEmailAddress(models.NewEmailAddress(loginForm.EmailAddress)) if err != nil { http.Error(responseWriter, err.Error(), http.StatusInternalServerError) return } - token, err := createTokenAsString(userId, credentialTimeoutDuration) + token, err := env.createTokenAsString(userId, credentialTimeoutDuration) if err != nil { http.Error(responseWriter, err.Error(), http.StatusInternalServerError) return @@ -224,7 +213,7 @@ func HandleSessionApiRequest( } } -func HandleNoteApiRequest( +func (env *Environment) HandleNoteApiRequest( responseWriter http.ResponseWriter, request *http.Request, userId models.UserId, @@ -280,7 +269,7 @@ func HandleNoteApiRequest( CreationTime: time.Now().UTC(), } - noteId, err := environment.Db.StoreNewNote(note) + noteId, err := env.Db.StoreNewNote(note) if err != nil { http.Error(responseWriter, err.Error(), http.StatusInternalServerError) return @@ -294,7 +283,7 @@ func HandleNoteApiRequest( } } -func HandleCategoryApiRequest( +func (env *Environment) HandleCategoryApiRequest( responseWriter http.ResponseWriter, request *http.Request, userId models.UserId, @@ -321,7 +310,7 @@ func HandleCategoryApiRequest( return } - if err := environment.Db.StoreNewNoteCategoryRelationship(models.NoteId(noteForm.NoteId), category); err != nil { + if err := env.Db.StoreNewNoteCategoryRelationship(models.NoteId(noteForm.NoteId), category); err != nil { http.Error(responseWriter, err.Error(), http.StatusInternalServerError) return } @@ -339,12 +328,12 @@ type AuthenticatedRequestHandlerType func( *http.Request, models.UserId) -func AuthenticateOrRedirect( +func (env *Environment) AuthenticateOrRedirect( authenticatedHandlerFunc AuthenticatedRequestHandlerType, redirectPath string, ) http.HandlerFunc { return func(responseWriter http.ResponseWriter, request *http.Request) { - if userId, err := getUserIdFromJwtToken(request); err != nil { + if userId, err := env.getUserIdFromJwtToken(request); err != nil { switch request.Method { // If not logged in, redirect to login page case http.MethodGet: @@ -363,12 +352,12 @@ func AuthenticateOrRedirect( } } -func AuthenticateOrReturnUnauthorized( +func (env *Environment) AuthenticateOrReturnUnauthorized( authenticatedHandlerFunc AuthenticatedRequestHandlerType, ) http.HandlerFunc { return func(responseWriter http.ResponseWriter, request *http.Request) { - if userId, err := getUserIdFromJwtToken(request); err != nil { + if userId, err := env.getUserIdFromJwtToken(request); err != nil { responseWriter.Header().Set("WWW-Authenticate", `Bearer realm="`+request.URL.Path+`"`) http.Error(responseWriter, err.Error(), http.StatusUnauthorized) } else { @@ -397,7 +386,7 @@ func RedirectToPathHandler( // AUTHENTICATED HANDLERS -func HandleHomePageRequest( +func (env *Environment) HandleHomePageRequest( responseWriter http.ResponseWriter, request *http.Request, userId models.UserId, @@ -416,7 +405,7 @@ func HandleHomePageRequest( } } -func HandleNotesPageRequest( +func (env *Environment) HandleNotesPageRequest( responseWriter http.ResponseWriter, request *http.Request, userId models.UserId, diff --git a/handlers/tokenutil.go b/handlers/tokenutil.go index 7d379c5..a9c0a7d 100644 --- a/handlers/tokenutil.go +++ b/handlers/tokenutil.go @@ -2,8 +2,6 @@ package handlers import ( "errors" - "fmt" - "log" "net/http" "strings" "time" @@ -14,16 +12,16 @@ import ( var InvalidJWTokenError = errors.New("Token was invalid or unreadable") -func parseTokenFromString(tokenAsString string) (*jwt.Token, error) { +func (env *Environment) parseTokenFromString(tokenAsString string) (*jwt.Token, error) { return jwt.ParseWithClaims( strings.TrimSpace(tokenAsString), &JwtTokenClaim{}, func(*jwt.Token) (interface{}, error) { - return tokenSigningKey, nil + return env.TokenSigningKey, nil }) } -func createTokenAsString( +func (env *Environment) createTokenAsString( userId models.UserId, durationTilExpiration time.Duration, ) (string, error) { @@ -36,16 +34,16 @@ func createTokenAsString( } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - return token.SignedString(tokenSigningKey) + return token.SignedString(env.TokenSigningKey) } -func getUserIdFromJwtToken(request *http.Request) (models.UserId, error) { +func (env *Environment) getUserIdFromJwtToken(request *http.Request) (models.UserId, error) { cookie, err := request.Cookie(cerealNotesCookieName) if err != nil { return 0, err } - token, err := parseTokenFromString(cookie.Value) + token, err := env.parseTokenFromString(cookie.Value) if err != nil { return 0, err } @@ -57,26 +55,26 @@ func getUserIdFromJwtToken(request *http.Request) (models.UserId, error) { return 0, InvalidJWTokenError } -func tokenTest1() { - var num models.UserId = 32 - bob, err := createTokenAsString(num, 1) - if err != nil { - fmt.Println("create error") - log.Fatal(err) - } +// func tokenTest1() { +// var num models.UserId = 32 +// bob, err := createTokenAsString(num, 1) +// if err != nil { +// fmt.Println("create error") +// log.Fatal(err) +// } - token, err := parseTokenFromString(bob) - if err != nil { - fmt.Println("parse error") - log.Fatal(err) - } - fmt.Println(bob) - if claims, ok := token.Claims.(*JwtTokenClaim); ok && token.Valid { - if claims.UserId != 32 { - log.Fatal("error in token") - } - fmt.Printf("%v %v", claims.UserId, claims.StandardClaims.ExpiresAt) - } else { - fmt.Println("Token claims could not be read") - } -} +// token, err := parseTokenFromString(bob) +// if err != nil { +// fmt.Println("parse error") +// log.Fatal(err) +// } +// fmt.Println(bob) +// if claims, ok := token.Claims.(*JwtTokenClaim); ok && token.Valid { +// if claims.UserId != 32 { +// log.Fatal("error in token") +// } +// fmt.Printf("%v %v", claims.UserId, claims.StandardClaims.ExpiresAt) +// } else { +// fmt.Println("Token claims could not be read") +// } +// } diff --git a/integration_test.go b/integration_test.go index 0fd1689..94889a9 100644 --- a/integration_test.go +++ b/integration_test.go @@ -19,12 +19,9 @@ import ( func TestLoginOrSignUpPage(t *testing.T) { mockDb := &DiyMockDataStore{} - env := &handlers.Environment{mockDb} + env := &handlers.Environment{mockDb, []byte("")} - handlers.SetEnvironment(env) - handlers.SetTokenSigningKey([]byte("")) - - server := httptest.NewServer(routers.DefineRoutes()) + server := httptest.NewServer(routers.DefineRoutes(env)) defer server.Close() resp, err := http.Get(server.URL) @@ -36,12 +33,9 @@ func TestLoginOrSignUpPage(t *testing.T) { func TestLoginApi(t *testing.T) { mockDb := &DiyMockDataStore{} - env := &handlers.Environment{mockDb} - - handlers.SetEnvironment(env) - handlers.SetTokenSigningKey([]byte("")) + env := &handlers.Environment{mockDb, []byte("")} - server := httptest.NewServer(routers.DefineRoutes()) + server := httptest.NewServer(routers.DefineRoutes(env)) defer server.Close() theEmail := "justsomeemail@gmail.com" diff --git a/main.go b/main.go index 32c31dc..f02a2f3 100644 --- a/main.go +++ b/main.go @@ -78,12 +78,7 @@ func main() { if err != nil { log.Fatal(err) } - - handlers.SetTokenSigningKey(tokenSigningKey) - } - - { - handlers.SetEnvironment(env) + env.TokenSigningKey = tokenSigningKey } // Start server @@ -95,7 +90,7 @@ func main() { log.Printf("Listening on %s...\n", port) - if err := http.ListenAndServe(port, routers.DefineRoutes()); err != nil { + if err := http.ListenAndServe(port, routers.DefineRoutes(env)); err != nil { log.Fatal(err) } } diff --git a/routers/routers.go b/routers/routers.go index 1c86168..f7c5657 100644 --- a/routers/routers.go +++ b/routers/routers.go @@ -12,25 +12,26 @@ import ( type routeHandler struct { *http.ServeMux + Env *handlers.Environment } func (mux *routeHandler) handleAuthenticatedPage( pattern string, handlerFunc handlers.AuthenticatedRequestHandlerType, ) { - mux.HandleFunc(pattern, handlers.AuthenticateOrRedirect(handlerFunc, paths.LoginOrSignupPage)) + mux.HandleFunc(pattern, mux.Env.AuthenticateOrRedirect(handlerFunc, paths.LoginOrSignupPage)) } func (mux *routeHandler) handleAuthenticatedApi( pattern string, handlerFunc handlers.AuthenticatedRequestHandlerType, ) { - mux.HandleFunc(pattern, handlers.AuthenticateOrReturnUnauthorized(handlerFunc)) + mux.HandleFunc(pattern, mux.Env.AuthenticateOrReturnUnauthorized(handlerFunc)) } // DefineRoutes returns a new servemux with all the required path and handler pairs attached. -func DefineRoutes() http.Handler { - mux := &routeHandler{http.NewServeMux()} +func DefineRoutes(env *handlers.Environment) http.Handler { + mux := &routeHandler{http.NewServeMux(), env} // static files { staticDirectoryName := "static" @@ -49,18 +50,18 @@ func DefineRoutes() http.Handler { mux.HandleFunc("/favicon.ico", handlers.RedirectToPathHandler("/static/favicon.ico")) // pages - mux.HandleFunc(paths.LoginOrSignupPage, handlers.HandleLoginOrSignupPageRequest) + mux.HandleFunc(paths.LoginOrSignupPage, env.HandleLoginOrSignupPageRequest) - mux.handleAuthenticatedPage(paths.HomePage, handlers.HandleHomePageRequest) - mux.handleAuthenticatedPage(paths.NotesPage, handlers.HandleNotesPageRequest) + mux.handleAuthenticatedPage(paths.HomePage, env.HandleHomePageRequest) + mux.handleAuthenticatedPage(paths.NotesPage, env.HandleNotesPageRequest) // api - mux.HandleFunc(paths.UserApi, handlers.HandleUserApiRequest) - mux.HandleFunc(paths.SessionApi, handlers.HandleSessionApiRequest) + mux.HandleFunc(paths.UserApi, env.HandleUserApiRequest) + mux.HandleFunc(paths.SessionApi, env.HandleSessionApiRequest) - mux.handleAuthenticatedApi(paths.NoteApi, handlers.HandleNoteApiRequest) - mux.handleAuthenticatedApi(paths.CategoryApi, handlers.HandleCategoryApiRequest) + mux.handleAuthenticatedApi(paths.NoteApi, env.HandleNoteApiRequest) + mux.handleAuthenticatedApi(paths.CategoryApi, env.HandleCategoryApiRequest) return mux } From 0be22489ef96cea91ee542ff4a86c2fb60216164 Mon Sep 17 00:00:00 2001 From: Giuseppe Date: Mon, 17 Sep 2018 00:22:40 -0700 Subject: [PATCH 05/20] all handlers aren't under environment --- handlers/handlers.go | 47 +++++++++++++++++++++++++++++++++----------- routers/routers.go | 31 ++++++++++++++++++----------- 2 files changed, 55 insertions(+), 23 deletions(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index bc5f6f8..a5cad11 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -31,11 +31,18 @@ type Environment struct { TokenSigningKey []byte } +func WrapUnauthenticatedEndpoint(env *Environment, handler UnauthenticatedEndpointHandlerType) http.HandlerFunc { + return func(responseWriter http.ResponseWriter, request *http.Request) { + handler(env, responseWriter, request) + } +} + // UNAUTHENTICATED HANDLERS // HandleLoginOrSignupPageRequest responds to unauthenticated GET requests with the login or signup page. // For authenticated requests, it redirects to the home page. -func (env *Environment) HandleLoginOrSignupPageRequest( +func HandleLoginOrSignupPageRequest( + env *Environment, responseWriter http.ResponseWriter, request *http.Request, ) { @@ -63,7 +70,8 @@ func (env *Environment) HandleLoginOrSignupPageRequest( } } -func (env *Environment) HandleUserApiRequest( +func HandleUserApiRequest( + env *Environment, responseWriter http.ResponseWriter, request *http.Request, ) { @@ -132,7 +140,8 @@ func (env *Environment) HandleUserApiRequest( // HandleSessionApiRequest responds to POST requests by authenticating and responding with a JWT. // It responds to DELETE requests by expiring the client's cookie. -func (env *Environment) HandleSessionApiRequest( +func HandleSessionApiRequest( + env *Environment, responseWriter http.ResponseWriter, request *http.Request, ) { @@ -213,7 +222,8 @@ func (env *Environment) HandleSessionApiRequest( } } -func (env *Environment) HandleNoteApiRequest( +func HandleNoteApiRequest( + env *Environment, responseWriter http.ResponseWriter, request *http.Request, userId models.UserId, @@ -283,7 +293,8 @@ func (env *Environment) HandleNoteApiRequest( } } -func (env *Environment) HandleCategoryApiRequest( +func HandleCategoryApiRequest( + env *Environment, responseWriter http.ResponseWriter, request *http.Request, userId models.UserId, @@ -324,11 +335,20 @@ func (env *Environment) HandleCategoryApiRequest( } type AuthenticatedRequestHandlerType func( + *Environment, http.ResponseWriter, *http.Request, - models.UserId) + models.UserId, +) + +type UnauthenticatedEndpointHandlerType func( + *Environment, + http.ResponseWriter, + *http.Request, +) -func (env *Environment) AuthenticateOrRedirect( +func AuthenticateOrRedirect( + env *Environment, authenticatedHandlerFunc AuthenticatedRequestHandlerType, redirectPath string, ) http.HandlerFunc { @@ -347,12 +367,13 @@ func (env *Environment) AuthenticateOrRedirect( respondWithMethodNotAllowed(responseWriter, http.MethodGet) } } else { - authenticatedHandlerFunc(responseWriter, request, userId) + authenticatedHandlerFunc(env, responseWriter, request, userId) } } } -func (env *Environment) AuthenticateOrReturnUnauthorized( +func AuthenticateOrReturnUnauthorized( + env *Environment, authenticatedHandlerFunc AuthenticatedRequestHandlerType, ) http.HandlerFunc { return func(responseWriter http.ResponseWriter, request *http.Request) { @@ -361,7 +382,7 @@ func (env *Environment) AuthenticateOrReturnUnauthorized( responseWriter.Header().Set("WWW-Authenticate", `Bearer realm="`+request.URL.Path+`"`) http.Error(responseWriter, err.Error(), http.StatusUnauthorized) } else { - authenticatedHandlerFunc(responseWriter, request, userId) + authenticatedHandlerFunc(env, responseWriter, request, userId) } } } @@ -386,7 +407,8 @@ func RedirectToPathHandler( // AUTHENTICATED HANDLERS -func (env *Environment) HandleHomePageRequest( +func HandleHomePageRequest( + env *Environment, responseWriter http.ResponseWriter, request *http.Request, userId models.UserId, @@ -405,7 +427,8 @@ func (env *Environment) HandleHomePageRequest( } } -func (env *Environment) HandleNotesPageRequest( +func HandleNotesPageRequest( + env *Environment, responseWriter http.ResponseWriter, request *http.Request, userId models.UserId, diff --git a/routers/routers.go b/routers/routers.go index f7c5657..21c0dcd 100644 --- a/routers/routers.go +++ b/routers/routers.go @@ -12,26 +12,35 @@ import ( type routeHandler struct { *http.ServeMux - Env *handlers.Environment } func (mux *routeHandler) handleAuthenticatedPage( + env *handlers.Environment, pattern string, handlerFunc handlers.AuthenticatedRequestHandlerType, ) { - mux.HandleFunc(pattern, mux.Env.AuthenticateOrRedirect(handlerFunc, paths.LoginOrSignupPage)) + mux.HandleFunc(pattern, handlers.AuthenticateOrRedirect(env, handlerFunc, paths.LoginOrSignupPage)) } func (mux *routeHandler) handleAuthenticatedApi( + env *handlers.Environment, pattern string, handlerFunc handlers.AuthenticatedRequestHandlerType, ) { - mux.HandleFunc(pattern, mux.Env.AuthenticateOrReturnUnauthorized(handlerFunc)) + mux.HandleFunc(pattern, handlers.AuthenticateOrReturnUnauthorized(env, handlerFunc)) +} + +func (mux *routeHandler) handleUnAutheticedRequest( + env *handlers.Environment, + pattern string, + handlerFunc handlers.UnauthenticatedEndpointHandlerType, +) { + mux.HandleFunc(pattern, handlers.WrapUnauthenticatedEndpoint(env, handlerFunc)) } // DefineRoutes returns a new servemux with all the required path and handler pairs attached. func DefineRoutes(env *handlers.Environment) http.Handler { - mux := &routeHandler{http.NewServeMux(), env} + mux := &routeHandler{http.NewServeMux()} // static files { staticDirectoryName := "static" @@ -50,18 +59,18 @@ func DefineRoutes(env *handlers.Environment) http.Handler { mux.HandleFunc("/favicon.ico", handlers.RedirectToPathHandler("/static/favicon.ico")) // pages - mux.HandleFunc(paths.LoginOrSignupPage, env.HandleLoginOrSignupPageRequest) + mux.handleUnAutheticedRequest(env, paths.LoginOrSignupPage, handlers.HandleLoginOrSignupPageRequest) - mux.handleAuthenticatedPage(paths.HomePage, env.HandleHomePageRequest) - mux.handleAuthenticatedPage(paths.NotesPage, env.HandleNotesPageRequest) + mux.handleAuthenticatedPage(env, paths.HomePage, handlers.HandleHomePageRequest) + mux.handleAuthenticatedPage(env, paths.NotesPage, handlers.HandleNotesPageRequest) // api - mux.HandleFunc(paths.UserApi, env.HandleUserApiRequest) - mux.HandleFunc(paths.SessionApi, env.HandleSessionApiRequest) + mux.handleUnAutheticedRequest(env, paths.UserApi, handlers.HandleUserApiRequest) + mux.handleUnAutheticedRequest(env, paths.SessionApi, handlers.HandleSessionApiRequest) - mux.handleAuthenticatedApi(paths.NoteApi, env.HandleNoteApiRequest) - mux.handleAuthenticatedApi(paths.CategoryApi, env.HandleCategoryApiRequest) + mux.handleAuthenticatedApi(env, paths.NoteApi, handlers.HandleNoteApiRequest) + mux.handleAuthenticatedApi(env, paths.CategoryApi, handlers.HandleCategoryApiRequest) return mux } From 64a7cebbcbb1409b998bc2525e003d284e74127f Mon Sep 17 00:00:00 2001 From: Giuseppe Date: Mon, 17 Sep 2018 01:20:56 -0700 Subject: [PATCH 06/20] added example of db testing --- README.md | 4 ++ migrations/tools/createTestDatabase.sql | 1 + migrations/tools/drop_everything.sql | 8 ++-- migrations/tools/truncate_tables.sql | 9 ++++ models/datastore_test.go | 53 ++++++++++++++++++++++++ models/note_test.go | 55 +++++++++++++++++++++++++ 6 files changed, 126 insertions(+), 4 deletions(-) create mode 100644 migrations/tools/createTestDatabase.sql create mode 100644 migrations/tools/truncate_tables.sql create mode 100644 models/datastore_test.go create mode 100644 models/note_test.go diff --git a/README.md b/README.md index fe1ec89..2a5cdbc 100644 --- a/README.md +++ b/README.md @@ -24,3 +24,7 @@ Assuming your local environment is setup correctly with Golang standards, you ca # Run DB migrations More db information in `migrations/README.md` + +# Setup Testing +1. `psql < /migrations/tools/createTestDatabase.sql` +2. run all migrations on `psql test_db` diff --git a/migrations/tools/createTestDatabase.sql b/migrations/tools/createTestDatabase.sql new file mode 100644 index 0000000..9553d17 --- /dev/null +++ b/migrations/tools/createTestDatabase.sql @@ -0,0 +1 @@ +CREATE DATABASE test_db; \ No newline at end of file diff --git a/migrations/tools/drop_everything.sql b/migrations/tools/drop_everything.sql index ac4fd8a..5463955 100644 --- a/migrations/tools/drop_everything.sql +++ b/migrations/tools/drop_everything.sql @@ -1,11 +1,11 @@ DROP TYPE category_type CASCADE; -DROP TABLE app_user CASCADE; +DROP TABLE note_to_category_relationship CASCADE; + +DROP TABLE note_to_publication_relationship CASCADE; DROP TABLE publication CASCADE; DROP TABLE note CASCADE; -DROP TABLE note_to_type_relationship CASCADE; - -DROP TABLE note_to_publication_relationship CASCADE; \ No newline at end of file +DROP TABLE app_user CASCADE; \ No newline at end of file diff --git a/migrations/tools/truncate_tables.sql b/migrations/tools/truncate_tables.sql new file mode 100644 index 0000000..f5b94c6 --- /dev/null +++ b/migrations/tools/truncate_tables.sql @@ -0,0 +1,9 @@ +TRUNCATE note_to_publication_relationship CASCADE; + +TRUNCATE publication CASCADE; + +TRUNCATE note_to_category_relationship CASCADE; + +TRUNCATE note CASCADE; + +TRUNCATE app_user CASCADE; \ No newline at end of file diff --git a/models/datastore_test.go b/models/datastore_test.go new file mode 100644 index 0000000..cc67501 --- /dev/null +++ b/models/datastore_test.go @@ -0,0 +1,53 @@ +package models_test + +import ( + "testing" + "time" + "strconv" + + "github.com/atmiguel/cerealnotes/models" +) + +var postgresUrl = "postgresql://localhost/test_db?sslmode=disable" + +func ClearAllValuesInTable(*models.DB) { + // todo call trucate_tables.sql +} + +func TestUser(t *testing.T) { + db, err := models.ConnectToDatabase(postgresUrl) + ok(t, err) + + displayName := "boby" + password := "aPassword" + emailAddress := models.NewEmailAddress("thisIsMyOtherEmail@gmail.com") + + err = db.StoreNewUser(displayName,emailAddress,password) + ok(t, err) + + _, err = db.GetIdForUserWithEmailAddress(emailAddress) + ok(t, err) + + err = db.AuthenticateUserCredentials(emailAddress, password) + ok(t, err) +} + +func TestNote(t *testing.T) { + db, err := models.ConnectToDatabase(postgresUrl) + ok(t, err) + + displayName := "bob" + password := "aPassword" + emailAddress := models.NewEmailAddress("thisIsMyEmail@gmail.com") + + err = db.StoreNewUser(displayName,emailAddress,password) + ok(t, err) + + userId, err := db.GetIdForUserWithEmailAddress(emailAddress) + ok(t, err) + + note := &models.Note{AuthorId: userId, Content: "I'm a note", CreationTime: time.Now()} + id, err := db.StoreNewNote(note) + ok(t, err) + assert(t, int64(id) > 0, "Note Id was not a valid index: "+strconv.Itoa(int(id))) +} diff --git a/models/note_test.go b/models/note_test.go new file mode 100644 index 0000000..fda8e04 --- /dev/null +++ b/models/note_test.go @@ -0,0 +1,55 @@ +package models_test + +import ( + "fmt" + "path/filepath" + "reflect" + "runtime" + "testing" + + "github.com/atmiguel/cerealnotes/models" +) + +var deserializationTests = []models.Category{ + models.MARGINALIA, + models.META, + models.QUESTIONS, + models.PREDICTIONS, +} + +func TestDeserialization(t *testing.T) { + for _, val := range deserializationTests { + t.Run(val.String(), func(t *testing.T) { + cat, err := models.DeserializeCategory(val.String()) + ok(t, err) + equals(t, val, cat) + }) + } + +} + +func assert(tb testing.TB, condition bool, msg string, v ...interface{}) { + if !condition { + _, file, line, _ := runtime.Caller(1) + fmt.Printf("\033[31m%s:%d: "+msg+"\033[39m\n\n", append([]interface{}{filepath.Base(file), line}, v...)...) + tb.FailNow() + } +} + +// ok fails the test if an err is not nil. +func ok(tb testing.TB, err error) { + if err != nil { + _, file, line, _ := runtime.Caller(1) + fmt.Printf("\033[31m%s:%d: unexpected error: %s\033[39m\n\n", filepath.Base(file), line, err.Error()) + tb.FailNow() + } +} + +// equals fails the test if exp is not equal to act. +func equals(tb testing.TB, exp, act interface{}) { + if !reflect.DeepEqual(exp, act) { + _, file, line, _ := runtime.Caller(1) + fmt.Printf("\033[31m%s:%d:\n\n\texp: %#v\n\n\tgot: %#v\033[39m\n\n", filepath.Base(file), line, exp, act) + tb.FailNow() + } +} From 07c8299afaf13044819dff06281a64107147976f Mon Sep 17 00:00:00 2001 From: G Date: Mon, 17 Sep 2018 10:15:55 -0700 Subject: [PATCH 07/20] added final db test, also have the test clear the Db before they run --- models/datastore_test.go | 65 +++++++++++++++++++++++++++++++++++----- 1 file changed, 58 insertions(+), 7 deletions(-) diff --git a/models/datastore_test.go b/models/datastore_test.go index cc67501..fc5afea 100644 --- a/models/datastore_test.go +++ b/models/datastore_test.go @@ -1,29 +1,56 @@ package models_test import ( + "fmt" + "strconv" "testing" "time" - "strconv" "github.com/atmiguel/cerealnotes/models" ) var postgresUrl = "postgresql://localhost/test_db?sslmode=disable" -func ClearAllValuesInTable(*models.DB) { - // todo call trucate_tables.sql +var tables = []string{ + "note_to_publication_relationship", + "publication", + "note_to_category_relationship", + "note", + "app_user", +} + +func ClearAllValuesInTable(db *models.DB) { + for _, val := range tables { + if err := ClearValuesInTable(db, val); err != nil { + panic(err) + } + } + +} + +func ClearValuesInTable(db *models.DB, table string) error { + // db.Query() doesn't allow varaibles to replace columns or table names. + sqlQuery := fmt.Sprintf(`TRUNCATE %s CASCADE;`, table) + + _, err := db.Exec(sqlQuery) + if err != nil { + return err + } + + return nil } func TestUser(t *testing.T) { db, err := models.ConnectToDatabase(postgresUrl) ok(t, err) + ClearAllValuesInTable(db) displayName := "boby" password := "aPassword" emailAddress := models.NewEmailAddress("thisIsMyOtherEmail@gmail.com") - err = db.StoreNewUser(displayName,emailAddress,password) - ok(t, err) + err = db.StoreNewUser(displayName, emailAddress, password) + ok(t, err) _, err = db.GetIdForUserWithEmailAddress(emailAddress) ok(t, err) @@ -35,13 +62,14 @@ func TestUser(t *testing.T) { func TestNote(t *testing.T) { db, err := models.ConnectToDatabase(postgresUrl) ok(t, err) + ClearAllValuesInTable(db) displayName := "bob" password := "aPassword" emailAddress := models.NewEmailAddress("thisIsMyEmail@gmail.com") - err = db.StoreNewUser(displayName,emailAddress,password) - ok(t, err) + err = db.StoreNewUser(displayName, emailAddress, password) + ok(t, err) userId, err := db.GetIdForUserWithEmailAddress(emailAddress) ok(t, err) @@ -51,3 +79,26 @@ func TestNote(t *testing.T) { ok(t, err) assert(t, int64(id) > 0, "Note Id was not a valid index: "+strconv.Itoa(int(id))) } + +func TestCategory(t *testing.T) { + db, err := models.ConnectToDatabase(postgresUrl) + ok(t, err) + ClearAllValuesInTable(db) + + displayName := "bob" + password := "aPassword" + emailAddress := models.NewEmailAddress("thisyetAnotherIsMyEmail@gmail.com") + + err = db.StoreNewUser(displayName, emailAddress, password) + ok(t, err) + + userId, err := db.GetIdForUserWithEmailAddress(emailAddress) + ok(t, err) + + note := &models.Note{AuthorId: userId, Content: "I'm a note", CreationTime: time.Now()} + noteId, err := db.StoreNewNote(note) + ok(t, err) + + err = db.StoreNewNoteCategoryRelationship(noteId, models.META) + ok(t, err) +} From f5732f9fd428ab2da18a5e9f87c57b9b5fbaaa75 Mon Sep 17 00:00:00 2001 From: G Date: Mon, 17 Sep 2018 10:53:27 -0700 Subject: [PATCH 08/20] added token util testing --- handlers/handlers.go | 10 +++--- handlers/tokenutil.go | 33 +++----------------- handlers/tokenutil_test.go | 64 ++++++++++++++++++++++++++++++++++++++ models/datastore_test.go | 29 +++++++++++++++++ models/note_test.go | 30 ------------------ 5 files changed, 103 insertions(+), 63 deletions(-) create mode 100644 handlers/tokenutil_test.go diff --git a/handlers/handlers.go b/handlers/handlers.go index a5cad11..2b540ef 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -48,7 +48,7 @@ func HandleLoginOrSignupPageRequest( ) { switch request.Method { case http.MethodGet: - if _, err := env.getUserIdFromJwtToken(request); err == nil { + if _, err := getUserIdFromJwtToken(env, request); err == nil { http.Redirect( responseWriter, request, @@ -110,7 +110,7 @@ func HandleUserApiRequest( case http.MethodGet: - if _, err := env.getUserIdFromJwtToken(request); err != nil { + if _, err := getUserIdFromJwtToken(env, request); err != nil { http.Error(responseWriter, err.Error(), http.StatusUnauthorized) return } @@ -179,7 +179,7 @@ func HandleSessionApiRequest( return } - token, err := env.createTokenAsString(userId, credentialTimeoutDuration) + token, err := CreateTokenAsString(env, userId, credentialTimeoutDuration) if err != nil { http.Error(responseWriter, err.Error(), http.StatusInternalServerError) return @@ -353,7 +353,7 @@ func AuthenticateOrRedirect( redirectPath string, ) http.HandlerFunc { return func(responseWriter http.ResponseWriter, request *http.Request) { - if userId, err := env.getUserIdFromJwtToken(request); err != nil { + if userId, err := getUserIdFromJwtToken(env, request); err != nil { switch request.Method { // If not logged in, redirect to login page case http.MethodGet: @@ -378,7 +378,7 @@ func AuthenticateOrReturnUnauthorized( ) http.HandlerFunc { return func(responseWriter http.ResponseWriter, request *http.Request) { - if userId, err := env.getUserIdFromJwtToken(request); err != nil { + if userId, err := getUserIdFromJwtToken(env, request); err != nil { responseWriter.Header().Set("WWW-Authenticate", `Bearer realm="`+request.URL.Path+`"`) http.Error(responseWriter, err.Error(), http.StatusUnauthorized) } else { diff --git a/handlers/tokenutil.go b/handlers/tokenutil.go index a9c0a7d..b4f2f6b 100644 --- a/handlers/tokenutil.go +++ b/handlers/tokenutil.go @@ -12,7 +12,7 @@ import ( var InvalidJWTokenError = errors.New("Token was invalid or unreadable") -func (env *Environment) parseTokenFromString(tokenAsString string) (*jwt.Token, error) { +func ParseTokenFromString(env *Environment, tokenAsString string) (*jwt.Token, error) { return jwt.ParseWithClaims( strings.TrimSpace(tokenAsString), &JwtTokenClaim{}, @@ -21,7 +21,8 @@ func (env *Environment) parseTokenFromString(tokenAsString string) (*jwt.Token, }) } -func (env *Environment) createTokenAsString( +func CreateTokenAsString( + env *Environment, userId models.UserId, durationTilExpiration time.Duration, ) (string, error) { @@ -37,13 +38,13 @@ func (env *Environment) createTokenAsString( return token.SignedString(env.TokenSigningKey) } -func (env *Environment) getUserIdFromJwtToken(request *http.Request) (models.UserId, error) { +func getUserIdFromJwtToken(env *Environment, request *http.Request) (models.UserId, error) { cookie, err := request.Cookie(cerealNotesCookieName) if err != nil { return 0, err } - token, err := env.parseTokenFromString(cookie.Value) + token, err := ParseTokenFromString(env, cookie.Value) if err != nil { return 0, err } @@ -54,27 +55,3 @@ func (env *Environment) getUserIdFromJwtToken(request *http.Request) (models.Use return 0, InvalidJWTokenError } - -// func tokenTest1() { -// var num models.UserId = 32 -// bob, err := createTokenAsString(num, 1) -// if err != nil { -// fmt.Println("create error") -// log.Fatal(err) -// } - -// token, err := parseTokenFromString(bob) -// if err != nil { -// fmt.Println("parse error") -// log.Fatal(err) -// } -// fmt.Println(bob) -// if claims, ok := token.Claims.(*JwtTokenClaim); ok && token.Valid { -// if claims.UserId != 32 { -// log.Fatal("error in token") -// } -// fmt.Printf("%v %v", claims.UserId, claims.StandardClaims.ExpiresAt) -// } else { -// fmt.Println("Token claims could not be read") -// } -// } diff --git a/handlers/tokenutil_test.go b/handlers/tokenutil_test.go new file mode 100644 index 0000000..3402fa4 --- /dev/null +++ b/handlers/tokenutil_test.go @@ -0,0 +1,64 @@ +package handlers_test + +import ( + "fmt" + "path/filepath" + "reflect" + "runtime" + "testing" + + "github.com/atmiguel/cerealnotes/handlers" + "github.com/atmiguel/cerealnotes/models" +) + +func TestToken(t *testing.T) { + env := &handlers.Environment{nil, []byte("TheWorld")} + + var num models.UserId = 32 + bob, err := handlers.CreateTokenAsString(env, num, 1) + if err != nil { + panic(err) + } + + token, err := handlers.ParseTokenFromString(env, bob) + if err != nil { + panic(err) + } + fmt.Println(bob) + if claims, ok := token.Claims.(*handlers.JwtTokenClaim); ok && token.Valid { + if claims.UserId != 32 { + fmt.Println("error in token") + t.FailNow() + } + fmt.Printf("%v %v", claims.UserId, claims.StandardClaims.ExpiresAt) + } else { + fmt.Println("Token claims could not be read") + t.FailNow() + } +} + +func assert(tb testing.TB, condition bool, msg string, v ...interface{}) { + if !condition { + _, file, line, _ := runtime.Caller(1) + fmt.Printf("\033[31m%s:%d: "+msg+"\033[39m\n\n", append([]interface{}{filepath.Base(file), line}, v...)...) + tb.FailNow() + } +} + +// ok fails the test if an err is not nil. +func ok(tb testing.TB, err error) { + if err != nil { + _, file, line, _ := runtime.Caller(1) + fmt.Printf("\033[31m%s:%d: unexpected error: %s\033[39m\n\n", filepath.Base(file), line, err.Error()) + tb.FailNow() + } +} + +// equals fails the test if exp is not equal to act. +func equals(tb testing.TB, exp, act interface{}) { + if !reflect.DeepEqual(exp, act) { + _, file, line, _ := runtime.Caller(1) + fmt.Printf("\033[31m%s:%d:\n\n\texp: %#v\n\n\tgot: %#v\033[39m\n\n", filepath.Base(file), line, exp, act) + tb.FailNow() + } +} diff --git a/models/datastore_test.go b/models/datastore_test.go index fc5afea..26a5ff3 100644 --- a/models/datastore_test.go +++ b/models/datastore_test.go @@ -2,6 +2,9 @@ package models_test import ( "fmt" + "path/filepath" + "reflect" + "runtime" "strconv" "testing" "time" @@ -102,3 +105,29 @@ func TestCategory(t *testing.T) { err = db.StoreNewNoteCategoryRelationship(noteId, models.META) ok(t, err) } + +func assert(tb testing.TB, condition bool, msg string, v ...interface{}) { + if !condition { + _, file, line, _ := runtime.Caller(1) + fmt.Printf("\033[31m%s:%d: "+msg+"\033[39m\n\n", append([]interface{}{filepath.Base(file), line}, v...)...) + tb.FailNow() + } +} + +// ok fails the test if an err is not nil. +func ok(tb testing.TB, err error) { + if err != nil { + _, file, line, _ := runtime.Caller(1) + fmt.Printf("\033[31m%s:%d: unexpected error: %s\033[39m\n\n", filepath.Base(file), line, err.Error()) + tb.FailNow() + } +} + +// equals fails the test if exp is not equal to act. +func equals(tb testing.TB, exp, act interface{}) { + if !reflect.DeepEqual(exp, act) { + _, file, line, _ := runtime.Caller(1) + fmt.Printf("\033[31m%s:%d:\n\n\texp: %#v\n\n\tgot: %#v\033[39m\n\n", filepath.Base(file), line, exp, act) + tb.FailNow() + } +} diff --git a/models/note_test.go b/models/note_test.go index fda8e04..8787761 100644 --- a/models/note_test.go +++ b/models/note_test.go @@ -1,10 +1,6 @@ package models_test import ( - "fmt" - "path/filepath" - "reflect" - "runtime" "testing" "github.com/atmiguel/cerealnotes/models" @@ -27,29 +23,3 @@ func TestDeserialization(t *testing.T) { } } - -func assert(tb testing.TB, condition bool, msg string, v ...interface{}) { - if !condition { - _, file, line, _ := runtime.Caller(1) - fmt.Printf("\033[31m%s:%d: "+msg+"\033[39m\n\n", append([]interface{}{filepath.Base(file), line}, v...)...) - tb.FailNow() - } -} - -// ok fails the test if an err is not nil. -func ok(tb testing.TB, err error) { - if err != nil { - _, file, line, _ := runtime.Caller(1) - fmt.Printf("\033[31m%s:%d: unexpected error: %s\033[39m\n\n", filepath.Base(file), line, err.Error()) - tb.FailNow() - } -} - -// equals fails the test if exp is not equal to act. -func equals(tb testing.TB, exp, act interface{}) { - if !reflect.DeepEqual(exp, act) { - _, file, line, _ := runtime.Caller(1) - fmt.Printf("\033[31m%s:%d:\n\n\texp: %#v\n\n\tgot: %#v\033[39m\n\n", filepath.Base(file), line, exp, act) - tb.FailNow() - } -} From 8718be8c13380df65a5854418cbf5c440413b102 Mon Sep 17 00:00:00 2001 From: G Date: Mon, 17 Sep 2018 14:53:02 -0700 Subject: [PATCH 09/20] add note tested --- handlers/handlers.go | 15 ++++++++++-- integration_test.go | 58 ++++++++++++++++++++++++++++++++++++++------ 2 files changed, 64 insertions(+), 9 deletions(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index 2b540ef..c59a045 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -5,7 +5,6 @@ import ( "fmt" "html/template" "net/http" - "strconv" "strings" "time" @@ -285,9 +284,21 @@ func HandleNoteApiRequest( return } - responseWriter.Write([]byte(`{NoteId: "` + strconv.FormatInt(int64(noteId), 10) + `"}`)) + type NoteResponse struct { + NoteId int64 `json:"noteId"` + } + + noteString, err := json.Marshal(&NoteResponse{NoteId: int64(noteId)}) + if err != nil { + http.Error(responseWriter, err.Error(), http.StatusInternalServerError) + return + } + + responseWriter.Header().Set("Content-Type", "application/json") responseWriter.WriteHeader(http.StatusCreated) + fmt.Fprint(responseWriter, string(noteString)) + default: respondWithMethodNotAllowed(responseWriter, http.MethodGet, http.MethodPost) } diff --git a/integration_test.go b/integration_test.go index 94889a9..4604644 100644 --- a/integration_test.go +++ b/integration_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "net/http" + "net/http/cookiejar" "net/http/httptest" "path/filepath" "reflect" @@ -28,16 +29,29 @@ func TestLoginOrSignUpPage(t *testing.T) { ok(t, err) // fmt.Println(ioutil.ReadAll(resp.Body)) - equals(t, 200, resp.StatusCode) + equals(t, http.StatusOK, resp.StatusCode) } -func TestLoginApi(t *testing.T) { +func TestAuthenticatedFlow(t *testing.T) { mockDb := &DiyMockDataStore{} env := &handlers.Environment{mockDb, []byte("")} server := httptest.NewServer(routers.DefineRoutes(env)) defer server.Close() + // Create testing client + // jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) + jar, err := cookiejar.New(&cookiejar.Options{}) + + if err != nil { + panic(err) + } + + client := &http.Client{ + Jar: jar, + } + + // Test login theEmail := "justsomeemail@gmail.com" thePassword := "worldsBestPassword" @@ -53,16 +67,46 @@ func TestLoginApi(t *testing.T) { return models.UserId(1), nil } - values := map[string]string{"emailAddress": theEmail, "password": thePassword} + userValues := map[string]string{"emailAddress": theEmail, "password": thePassword} - jsonValue, _ := json.Marshal(values) + userJsonValue, _ := json.Marshal(userValues) - resp, err := http.Post(server.URL+paths.SessionApi, "application/json", bytes.NewBuffer(jsonValue)) + resp, err := client.Post(server.URL+paths.SessionApi, "application/json", bytes.NewBuffer(userJsonValue)) ok(t, err) - // fmt.Println(ioutil.ReadAll(resp.Body)) - equals(t, 201, resp.StatusCode) + equals(t, http.StatusCreated, resp.StatusCode) + + // Test Add Note + noteValues := map[string]string{"content": "Dude I just said something cool"} + noteIdAsInt := int64(33) + + mockDb.Func_StoreNewNote = func(*models.Note) (models.NoteId, error) { + return models.NoteId(noteIdAsInt), nil + } + + noteJsonValue, _ := json.Marshal(noteValues) + + resp, err = client.Post(server.URL+paths.NoteApi, "application/json", bytes.NewBuffer(noteJsonValue)) + ok(t, err) + equals(t, http.StatusCreated, resp.StatusCode) + + type NoteResponse struct { + NoteId int64 `json:"noteId"` + } + + jsonNoteReponse := &NoteResponse{} + + err = json.NewDecoder(resp.Body).Decode(jsonNoteReponse) + ok(t, err) + + // bodyBytes, err := ioutil.ReadAll(resp.Body) + // fmt.Println(string(bodyBytes)) + + equals(t, noteIdAsInt, jsonNoteReponse.NoteId) + + resp.Body.Close() + } // Helpers From 264afcf7ae0cb56c5b2ab6a02be1cdf0ab78eef8 Mon Sep 17 00:00:00 2001 From: G Date: Mon, 17 Sep 2018 15:04:54 -0700 Subject: [PATCH 10/20] imporved organizatio + added content test --- integration_test.go | 97 ++++++++++++++++++++++++++++++--------------- models/category.go | 40 +++++++++++++++++++ models/note.go | 37 ----------------- 3 files changed, 104 insertions(+), 70 deletions(-) diff --git a/integration_test.go b/integration_test.go index 4604644..e828296 100644 --- a/integration_test.go +++ b/integration_test.go @@ -3,6 +3,7 @@ package main_test import ( "bytes" "encoding/json" + "errors" "fmt" "net/http" "net/http/cookiejar" @@ -52,60 +53,90 @@ func TestAuthenticatedFlow(t *testing.T) { } // Test login - theEmail := "justsomeemail@gmail.com" - thePassword := "worldsBestPassword" + userIdAsInt := int64(1) + { + theEmail := "justsomeemail@gmail.com" + thePassword := "worldsBestPassword" - mockDb.Func_AuthenticateUserCredentials = func(email *models.EmailAddress, password string) error { - if email.String() == theEmail && password == thePassword { - return nil - } + mockDb.Func_AuthenticateUserCredentials = func(email *models.EmailAddress, password string) error { + if email.String() == theEmail && password == thePassword { + return nil + } - return models.CredentialsNotAuthorizedError - } + return models.CredentialsNotAuthorizedError + } - mockDb.Func_GetIdForUserWithEmailAddress = func(email *models.EmailAddress) (models.UserId, error) { - return models.UserId(1), nil - } + mockDb.Func_GetIdForUserWithEmailAddress = func(email *models.EmailAddress) (models.UserId, error) { + return models.UserId(userIdAsInt), nil + } - userValues := map[string]string{"emailAddress": theEmail, "password": thePassword} + userValues := map[string]string{"emailAddress": theEmail, "password": thePassword} - userJsonValue, _ := json.Marshal(userValues) + userJsonValue, _ := json.Marshal(userValues) - resp, err := client.Post(server.URL+paths.SessionApi, "application/json", bytes.NewBuffer(userJsonValue)) + resp, err := client.Post(server.URL+paths.SessionApi, "application/json", bytes.NewBuffer(userJsonValue)) - ok(t, err) + ok(t, err) - equals(t, http.StatusCreated, resp.StatusCode) + equals(t, http.StatusCreated, resp.StatusCode) + } // Test Add Note - noteValues := map[string]string{"content": "Dude I just said something cool"} noteIdAsInt := int64(33) - mockDb.Func_StoreNewNote = func(*models.Note) (models.NoteId, error) { - return models.NoteId(noteIdAsInt), nil - } + { + noteValues := map[string]string{"content": "Dude I just said something cool"} + + mockDb.Func_StoreNewNote = func(*models.Note) (models.NoteId, error) { + return models.NoteId(noteIdAsInt), nil + } - noteJsonValue, _ := json.Marshal(noteValues) + noteJsonValue, _ := json.Marshal(noteValues) - resp, err = client.Post(server.URL+paths.NoteApi, "application/json", bytes.NewBuffer(noteJsonValue)) - ok(t, err) - equals(t, http.StatusCreated, resp.StatusCode) + resp, err := client.Post(server.URL+paths.NoteApi, "application/json", bytes.NewBuffer(noteJsonValue)) + ok(t, err) + equals(t, http.StatusCreated, resp.StatusCode) - type NoteResponse struct { - NoteId int64 `json:"noteId"` + type NoteResponse struct { + NoteId int64 `json:"noteId"` + } + + jsonNoteReponse := &NoteResponse{} + + err = json.NewDecoder(resp.Body).Decode(jsonNoteReponse) + ok(t, err) + + equals(t, noteIdAsInt, jsonNoteReponse.NoteId) + + resp.Body.Close() } - jsonNoteReponse := &NoteResponse{} + // Test Add category + { + type CategoryForm struct { + NoteId int64 `json:"noteId"` + Category string `json:"category"` + } - err = json.NewDecoder(resp.Body).Decode(jsonNoteReponse) - ok(t, err) + metaCategory := models.META - // bodyBytes, err := ioutil.ReadAll(resp.Body) - // fmt.Println(string(bodyBytes)) + categoryForm := &CategoryForm{NoteId: noteIdAsInt, Category: metaCategory.String()} - equals(t, noteIdAsInt, jsonNoteReponse.NoteId) + mockDb.Func_StoreNewNoteCategoryRelationship = func(noteId models.NoteId, cat models.Category) error { + if int64(noteId) == noteIdAsInt && cat == metaCategory { + return nil + } - resp.Body.Close() + return errors.New("Incorrect Data arrived") + } + + jsonValue, _ := json.Marshal(categoryForm) + + resp, err := client.Post(server.URL+paths.CategoryApi, "application/json", bytes.NewBuffer(jsonValue)) + ok(t, err) + equals(t, http.StatusCreated, resp.StatusCode) + + } } diff --git a/models/category.go b/models/category.go index c8b2571..af4cce5 100644 --- a/models/category.go +++ b/models/category.go @@ -1,5 +1,45 @@ package models +import ( + "errors" +) + +type Category int + +const ( + MARGINALIA Category = iota + META + QUESTIONS + PREDICTIONS +) + +var categoryStrings = [...]string{ + "marginalia", + "meta", + "questions", + "predictions", +} + +var CannotDeserializeCategoryStringError = errors.New("String does not correspond to a Note Category") + +func DeserializeCategory(input string) (Category, error) { + for i := 0; i < len(categoryStrings); i++ { + if input == categoryStrings[i] { + return Category(i), nil + } + } + return MARGINALIA, CannotDeserializeCategoryStringError +} + +func (category Category) String() string { + + if category < MARGINALIA || category > PREDICTIONS { + return "Unknown" + } + + return categoryStrings[category] +} + func (db *DB) StoreNewNoteCategoryRelationship( noteId NoteId, category Category, diff --git a/models/note.go b/models/note.go index abed956..ae1f516 100644 --- a/models/note.go +++ b/models/note.go @@ -1,48 +1,11 @@ package models import ( - "errors" "time" ) type NoteId int64 -type Category int - -const ( - MARGINALIA Category = iota - META - QUESTIONS - PREDICTIONS -) - -var categoryStrings = [...]string{ - "marginalia", - "meta", - "questions", - "predictions", -} - -var CannotDeserializeCategoryStringError = errors.New("String does not correspond to a Note Category") - -func DeserializeCategory(input string) (Category, error) { - for i := 0; i < len(categoryStrings); i++ { - if input == categoryStrings[i] { - return Category(i), nil - } - } - return MARGINALIA, CannotDeserializeCategoryStringError -} - -func (category Category) String() string { - - if category < MARGINALIA || category > PREDICTIONS { - return "Unknown" - } - - return categoryStrings[category] -} - type Note struct { AuthorId UserId `json:"authorId"` Content string `json:"content"` From cbe3283f3e4614fd4a41180cab7eee0346f8d330 Mon Sep 17 00:00:00 2001 From: G Date: Mon, 17 Sep 2018 15:50:55 -0700 Subject: [PATCH 11/20] better table clearage --- integration_test.go | 4 +--- models/datastore_test.go | 26 +++++++++++++++++--------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/integration_test.go b/integration_test.go index e828296..3e2e8f8 100644 --- a/integration_test.go +++ b/integration_test.go @@ -83,7 +83,6 @@ func TestAuthenticatedFlow(t *testing.T) { // Test Add Note noteIdAsInt := int64(33) - { noteValues := map[string]string{"content": "Dude I just said something cool"} @@ -127,7 +126,7 @@ func TestAuthenticatedFlow(t *testing.T) { return nil } - return errors.New("Incorrect Data arrived") + return errors.New("Incorrect Data Arrived") } jsonValue, _ := json.Marshal(categoryForm) @@ -135,7 +134,6 @@ func TestAuthenticatedFlow(t *testing.T) { resp, err := client.Post(server.URL+paths.CategoryApi, "application/json", bytes.NewBuffer(jsonValue)) ok(t, err) equals(t, http.StatusCreated, resp.StatusCode) - } } diff --git a/models/datastore_test.go b/models/datastore_test.go index 26a5ff3..758f9d2 100644 --- a/models/datastore_test.go +++ b/models/datastore_test.go @@ -14,12 +14,18 @@ import ( var postgresUrl = "postgresql://localhost/test_db?sslmode=disable" +const noteTable = "note" +const publicationTable = "publication" +const noteToPublicationTable = "note_to_publication_relationship" +const noteToCategoryTable = "note_to_category_relationship" +const userTable = "app_user" + var tables = []string{ - "note_to_publication_relationship", - "publication", - "note_to_category_relationship", - "note", - "app_user", + noteToPublicationTable, + publicationTable, + noteToCategoryTable, + noteTable, + userTable, } func ClearAllValuesInTable(db *models.DB) { @@ -28,7 +34,6 @@ func ClearAllValuesInTable(db *models.DB) { panic(err) } } - } func ClearValuesInTable(db *models.DB, table string) error { @@ -46,7 +51,7 @@ func ClearValuesInTable(db *models.DB, table string) error { func TestUser(t *testing.T) { db, err := models.ConnectToDatabase(postgresUrl) ok(t, err) - ClearAllValuesInTable(db) + ClearValuesInTable(db, userTable) displayName := "boby" password := "aPassword" @@ -65,7 +70,8 @@ func TestUser(t *testing.T) { func TestNote(t *testing.T) { db, err := models.ConnectToDatabase(postgresUrl) ok(t, err) - ClearAllValuesInTable(db) + ClearValuesInTable(db, userTable) + ClearValuesInTable(db, noteTable) displayName := "bob" password := "aPassword" @@ -86,7 +92,9 @@ func TestNote(t *testing.T) { func TestCategory(t *testing.T) { db, err := models.ConnectToDatabase(postgresUrl) ok(t, err) - ClearAllValuesInTable(db) + ClearValuesInTable(db, userTable) + ClearValuesInTable(db, noteTable) + ClearValuesInTable(db, noteToCategoryTable) displayName := "bob" password := "aPassword" From 8c0f362a2922592fa8c3f87a4a3bc86ff73b2dbd Mon Sep 17 00:00:00 2001 From: G Date: Tue, 18 Sep 2018 10:00:55 -0700 Subject: [PATCH 12/20] imporved readme/installation instructions --- .env | 2 +- README.md | 13 ++----------- migrations/README.md | 14 ++++++++------ migrations/tools/createDatabases.sql | 2 ++ migrations/tools/createTestDatabase.sql | 1 - models/datastore_test.go | 2 +- 6 files changed, 14 insertions(+), 20 deletions(-) create mode 100644 migrations/tools/createDatabases.sql delete mode 100644 migrations/tools/createTestDatabase.sql diff --git a/.env b/.env index 7e43ac9..e45c7b4 100644 --- a/.env +++ b/.env @@ -1,3 +1,3 @@ -DATABASE_URL='postgresql://localhost?sslmode=disable' +DATABASE_URL='postgresql://localhost/cerealnotes?sslmode=disable' PORT=8080 TOKEN_SIGNING_KEY='AllYourBase' diff --git a/README.md b/README.md index 2a5cdbc..7bff1d2 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ # Installation ## Locally -* postgres server installed and running - * `brew install postgres` - * `pg_ctl -D /usr/local/var/postgres start` +* postgres server installed and running: please refer to `migrations/README.md` for more info * heroku cli installed * `brew install heroku` * golang installed @@ -20,11 +18,4 @@ Assuming your local environment is setup correctly with Golang standards, you ca 1. `cd to this repo` 2. `go install && heroku local` -3. Visit `localhost:8080/login-or-signup` - -# Run DB migrations -More db information in `migrations/README.md` - -# Setup Testing -1. `psql < /migrations/tools/createTestDatabase.sql` -2. run all migrations on `psql test_db` +3. Visit `localhost:8080/` diff --git a/migrations/README.md b/migrations/README.md index 9111fcf..6888072 100644 --- a/migrations/README.md +++ b/migrations/README.md @@ -1,9 +1,11 @@ -# Locally: +# Locally: 1. first make sure that postgres is running. - * If installed via homebrew on a macOS: `pg_ctl -D /usr/local/var/postgres start` -2. then run migration locally - * `psql < *MIGRATION_NAME*` + * If installed via homebrew on a macOS: `pg_ctl start -D /usr/local/var/postgres` +2. then make sure you have the neccisary databases + * `psql < tools/createDatabases.sql` +3. then run all the migrations on both unittest database (`cerealnotes_test`) and as well as the "live" (`cerealnotes`)database. + * `psql [DATABASENAME] < [MIGRATION_NAME]` -# On Heroku: +# On Heroku: -1. `heroku pg:psql < *MIGRATION_NAME*` +1. `heroku pg:psql < [MIGRATION_NAME]` diff --git a/migrations/tools/createDatabases.sql b/migrations/tools/createDatabases.sql new file mode 100644 index 0000000..6e6b188 --- /dev/null +++ b/migrations/tools/createDatabases.sql @@ -0,0 +1,2 @@ +CREATE DATABASE cerealnotes_test; +CREATE DATABASE cerealnotes; \ No newline at end of file diff --git a/migrations/tools/createTestDatabase.sql b/migrations/tools/createTestDatabase.sql deleted file mode 100644 index 9553d17..0000000 --- a/migrations/tools/createTestDatabase.sql +++ /dev/null @@ -1 +0,0 @@ -CREATE DATABASE test_db; \ No newline at end of file diff --git a/models/datastore_test.go b/models/datastore_test.go index 758f9d2..ff21c92 100644 --- a/models/datastore_test.go +++ b/models/datastore_test.go @@ -12,7 +12,7 @@ import ( "github.com/atmiguel/cerealnotes/models" ) -var postgresUrl = "postgresql://localhost/test_db?sslmode=disable" +var postgresUrl = "postgresql://localhost/cerealnotes_test?sslmode=disable" const noteTable = "note" const publicationTable = "publication" From 8ec29b107b6cacbb97ac8b3a1dfbc836ddc4343a Mon Sep 17 00:00:00 2001 From: G Date: Tue, 18 Sep 2018 10:10:53 -0700 Subject: [PATCH 13/20] setup postgres readme update --- migrations/README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/migrations/README.md b/migrations/README.md index 6888072..0f14ca0 100644 --- a/migrations/README.md +++ b/migrations/README.md @@ -1,9 +1,12 @@ # Locally: -1. first make sure that postgres is running. - * If installed via homebrew on a macOS: `pg_ctl start -D /usr/local/var/postgres` -2. then make sure you have the neccisary databases +1. install & setup postgres + * `brew install postgres` + * `createdb ``whoami`` ` +2. Run postgres daemon. + * `pg_ctl start -D /usr/local/var/postgres` +3. Create cerealnotes databases * `psql < tools/createDatabases.sql` -3. then run all the migrations on both unittest database (`cerealnotes_test`) and as well as the "live" (`cerealnotes`)database. +3. Run all the migrations on both "unittest" database (`cerealnotes_test`) and as well as the "live" (`cerealnotes`)database. * `psql [DATABASENAME] < [MIGRATION_NAME]` # On Heroku: From 0b1df8a16113529b06a229b3960ffc3cba8fae19 Mon Sep 17 00:00:00 2001 From: G Date: Tue, 18 Sep 2018 10:11:57 -0700 Subject: [PATCH 14/20] typo --- migrations/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/migrations/README.md b/migrations/README.md index 0f14ca0..8497042 100644 --- a/migrations/README.md +++ b/migrations/README.md @@ -6,9 +6,9 @@ * `pg_ctl start -D /usr/local/var/postgres` 3. Create cerealnotes databases * `psql < tools/createDatabases.sql` -3. Run all the migrations on both "unittest" database (`cerealnotes_test`) and as well as the "live" (`cerealnotes`)database. +3. Run all the migrations on both "unittest" database (`cerealnotes_test`) and as well as the "live" database (`cerealnotes`). * `psql [DATABASENAME] < [MIGRATION_NAME]` # On Heroku: -1. `heroku pg:psql < [MIGRATION_NAME]` +1. `heroku pg:psql < [MIGRATION_NAME]` \ No newline at end of file From 347cf33684bba53680de0b3c98a91625ef84ff02 Mon Sep 17 00:00:00 2001 From: G Date: Tue, 18 Sep 2018 10:13:23 -0700 Subject: [PATCH 15/20] fixed markup --- migrations/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/README.md b/migrations/README.md index 8497042..0de104c 100644 --- a/migrations/README.md +++ b/migrations/README.md @@ -1,7 +1,7 @@ # Locally: 1. install & setup postgres * `brew install postgres` - * `createdb ``whoami`` ` + * ``createdb `whoami` `` 2. Run postgres daemon. * `pg_ctl start -D /usr/local/var/postgres` 3. Create cerealnotes databases From 7e0f4cf93d5469031e3af76e2c20cdd5a27b55c6 Mon Sep 17 00:00:00 2001 From: G Date: Tue, 18 Sep 2018 10:39:31 -0700 Subject: [PATCH 16/20] updated notes code update --- static/js/notes.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/static/js/notes.js b/static/js/notes.js index bb8bff9..a846d09 100644 --- a/static/js/notes.js +++ b/static/js/notes.js @@ -22,7 +22,7 @@ const $createDivider = function() { return $('', {text: ' - '}); }; -const $createNote = function(note) { +const $createNote = function(noteId, note) { const $author = $createAuthor(note.authorId); const $type = $createType(note.type); const $creationTime = $createCreationTime(note.creationTime); @@ -45,11 +45,13 @@ $(function() { $.get('/api/note', function(notes) { const $notes = $('#notes'); - notes.forEach((note) => { - $notes.append( - $createNote(note) - ); - }); + for (var key in notes) { + if (notes.hasOwnProperty(key)) { + $notes.append( + $createNote(key, notes[key]) + ); + } + } }); }); }); From d182494212c2f0092cd51fea3806590b5760ab7c Mon Sep 17 00:00:00 2001 From: G Date: Tue, 18 Sep 2018 10:51:01 -0700 Subject: [PATCH 17/20] a more readable js code --- static/js/notes.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/static/js/notes.js b/static/js/notes.js index a846d09..5d0945f 100644 --- a/static/js/notes.js +++ b/static/js/notes.js @@ -45,12 +45,9 @@ $(function() { $.get('/api/note', function(notes) { const $notes = $('#notes'); - for (var key in notes) { - if (notes.hasOwnProperty(key)) { - $notes.append( - $createNote(key, notes[key]) - ); - } + + for (const key of Object.keys(notes)) { + $notes.append($createNote(key, notes[key])); } }); }); From d03a79580e7c6d6f91cfabdb2c8781503a419e5d Mon Sep 17 00:00:00 2001 From: G Date: Tue, 18 Sep 2018 10:58:12 -0700 Subject: [PATCH 18/20] some readability improvments --- integration_test.go | 29 +++++++++++++++++++---------- static/js/notes.js | 1 - 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/integration_test.go b/integration_test.go index 3e2e8f8..9aa0e8f 100644 --- a/integration_test.go +++ b/integration_test.go @@ -28,8 +28,6 @@ func TestLoginOrSignUpPage(t *testing.T) { resp, err := http.Get(server.URL) ok(t, err) - - // fmt.Println(ioutil.ReadAll(resp.Body)) equals(t, http.StatusOK, resp.StatusCode) } @@ -41,15 +39,16 @@ func TestAuthenticatedFlow(t *testing.T) { defer server.Close() // Create testing client - // jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) - jar, err := cookiejar.New(&cookiejar.Options{}) + client := &http.Client{} + { + // jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) + jar, err := cookiejar.New(&cookiejar.Options{}) - if err != nil { - panic(err) - } + if err != nil { + panic(err) + } - client := &http.Client{ - Jar: jar, + client.Jar = jar } // Test login @@ -83,8 +82,9 @@ func TestAuthenticatedFlow(t *testing.T) { // Test Add Note noteIdAsInt := int64(33) + content := "Duuude I just said something cool" { - noteValues := map[string]string{"content": "Dude I just said something cool"} + noteValues := map[string]string{"content": content} mockDb.Func_StoreNewNote = func(*models.Note) (models.NoteId, error) { return models.NoteId(noteIdAsInt), nil @@ -110,6 +110,15 @@ func TestAuthenticatedFlow(t *testing.T) { resp.Body.Close() } + // Test get notes + { + resp, err := client.Get(server.URL + paths.NoteApi) + ok(t, err) + equals(t, http.StatusOK, resp.StatusCode) + + // TODO when we implement a real get notes feature we should enhance this code. + } + // Test Add category { type CategoryForm struct { diff --git a/static/js/notes.js b/static/js/notes.js index 5d0945f..f3d1d38 100644 --- a/static/js/notes.js +++ b/static/js/notes.js @@ -45,7 +45,6 @@ $(function() { $.get('/api/note', function(notes) { const $notes = $('#notes'); - for (const key of Object.keys(notes)) { $notes.append($createNote(key, notes[key])); } From dcadb91b95a5ed4622dad6212b8c804c3f4b4edd Mon Sep 17 00:00:00 2001 From: G Date: Wed, 26 Sep 2018 17:15:54 -0700 Subject: [PATCH 19/20] test ran succesfully --- handlers/handlers.go | 1 + integration_test.go | 32 +++++++++++++++++-- services/noteservice/noteservice.go | 48 ----------------------------- 3 files changed, 30 insertions(+), 51 deletions(-) delete mode 100644 services/noteservice/noteservice.go diff --git a/handlers/handlers.go b/handlers/handlers.go index 755b83d..a172f53 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -5,6 +5,7 @@ import ( "fmt" "html/template" "net/http" + "strconv" "strings" "time" diff --git a/integration_test.go b/integration_test.go index 9aa0e8f..910e178 100644 --- a/integration_test.go +++ b/integration_test.go @@ -5,12 +5,15 @@ import ( "encoding/json" "errors" "fmt" + "io" + "io/ioutil" "net/http" "net/http/cookiejar" "net/http/httptest" "path/filepath" "reflect" "runtime" + "strconv" "testing" "github.com/atmiguel/cerealnotes/handlers" @@ -122,13 +125,12 @@ func TestAuthenticatedFlow(t *testing.T) { // Test Add category { type CategoryForm struct { - NoteId int64 `json:"noteId"` Category string `json:"category"` } metaCategory := models.META - categoryForm := &CategoryForm{NoteId: noteIdAsInt, Category: metaCategory.String()} + categoryForm := &CategoryForm{Category: metaCategory.String()} mockDb.Func_StoreNewNoteCategoryRelationship = func(noteId models.NoteId, cat models.Category) error { if int64(noteId) == noteIdAsInt && cat == metaCategory { @@ -140,13 +142,37 @@ func TestAuthenticatedFlow(t *testing.T) { jsonValue, _ := json.Marshal(categoryForm) - resp, err := client.Post(server.URL+paths.CategoryApi, "application/json", bytes.NewBuffer(jsonValue)) + resp, err := sendPutRequest(client, server.URL+paths.CategoryApi+"?id="+strconv.FormatInt(noteIdAsInt, 10), "application/json", bytes.NewBuffer(jsonValue)) ok(t, err) equals(t, http.StatusCreated, resp.StatusCode) } } +func sendPutRequest(client *http.Client, myUrl string, contentType string, body io.Reader) (resp *http.Response, err error) { + req, err := http.NewRequest("PUT", myUrl, body) + + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", contentType) + return client.Do(req) +} + +func printBody(resp *http.Response) { + buf, bodyErr := ioutil.ReadAll(resp.Body) + if bodyErr != nil { + fmt.Print("bodyErr ", bodyErr.Error()) + return + } + + rdr1 := ioutil.NopCloser(bytes.NewBuffer(buf)) + rdr2 := ioutil.NopCloser(bytes.NewBuffer(buf)) + fmt.Printf("BODY: %q", rdr1) + resp.Body = rdr2 +} + // Helpers type DiyMockDataStore struct { diff --git a/services/noteservice/noteservice.go b/services/noteservice/noteservice.go deleted file mode 100644 index dede981..0000000 --- a/services/noteservice/noteservice.go +++ /dev/null @@ -1,48 +0,0 @@ -/* -Package noteservice handles interactions with database layer. -*/ -package noteservice - -import ( - "encoding/json" - "fmt" - - "github.com/atmiguel/cerealnotes/databaseutil" - "github.com/atmiguel/cerealnotes/models" -) - -func StoreNewNote( - note *models.Note, -) (models.NoteId, error) { - - id, err := databaseutil.InsertNewNote(int64(note.AuthorId), note.Content, note.CreationTime) - if err != nil { - return models.NoteId(0), err - } - - return models.NoteId(id), nil -} - -func StoreNewNoteCategoryRelationship( - noteId models.NoteId, - category models.Category, -) error { - if err := databaseutil.InsertNoteCategoryRelationship(int64(noteId), category.String()); err != nil { - return err - } - - return nil -} - -type NotesById map[models.NoteId]*models.Note - -func (notesById NotesById) ToJson() ([]byte, error) { - // json doesn't support int indexed maps - notesByIdString := make(map[string]models.Note, len(notesById)) - - for id, note := range notesById { - notesByIdString[fmt.Sprint(id)] = *note - } - - return json.Marshal(notesByIdString) -} From 564c039be112dce8cea83fd37cad7a193b26eca2 Mon Sep 17 00:00:00 2001 From: G Date: Mon, 1 Oct 2018 10:04:26 -0700 Subject: [PATCH 20/20] Category -> NoteCategory --- handlers/handlers.go | 8 ++++---- integration_test.go | 18 +++++++++--------- models/datastore.go | 2 +- models/{category.go => note_category.go} | 16 ++++++++-------- models/note_test.go | 4 ++-- paths/paths.go | 8 ++++---- routers/routers.go | 2 +- 7 files changed, 29 insertions(+), 29 deletions(-) rename models/{category.go => note_category.go} (67%) diff --git a/handlers/handlers.go b/handlers/handlers.go index a172f53..a8a446c 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -317,18 +317,18 @@ func HandleNoteCateogryApiRequest( id, err := strconv.ParseInt(request.URL.Query().Get("id"), 10, 64) noteId := models.NoteId(id) - type CategoryForm struct { - Category string `json:"category"` + type NoteCategoryForm struct { + NoteCategory string `json:"category"` } - categoryForm := new(CategoryForm) + categoryForm := new(NoteCategoryForm) if err := json.NewDecoder(request.Body).Decode(categoryForm); err != nil { http.Error(responseWriter, err.Error(), http.StatusBadRequest) return } - category, err := models.DeserializeCategory(categoryForm.Category) + category, err := models.DeserializeNoteCategory(categoryForm.NoteCategory) if err != nil { http.Error(responseWriter, err.Error(), http.StatusBadRequest) diff --git a/integration_test.go b/integration_test.go index 8417561..434fb7a 100644 --- a/integration_test.go +++ b/integration_test.go @@ -121,16 +121,16 @@ func TestAuthenticatedFlow(t *testing.T) { // Test Add category { - type CategoryForm struct { - Category string `json:"category"` + type NoteCategoryForm struct { + NoteCategory string `json:"category"` } - metaCategory := models.META + metaNoteCategory := models.META - categoryForm := &CategoryForm{Category: metaCategory.String()} + categoryForm := &NoteCategoryForm{NoteCategory: metaNoteCategory.String()} - mockDb.Func_StoreNewNoteCategoryRelationship = func(noteId models.NoteId, cat models.Category) error { - if int64(noteId) == noteIdAsInt && cat == metaCategory { + mockDb.Func_StoreNewNoteCategoryRelationship = func(noteId models.NoteId, cat models.NoteCategory) error { + if int64(noteId) == noteIdAsInt && cat == metaNoteCategory { return nil } @@ -139,7 +139,7 @@ func TestAuthenticatedFlow(t *testing.T) { jsonValue, _ := json.Marshal(categoryForm) - resp, err := sendPutRequest(client, server.URL+paths.CategoryApi+"?id="+strconv.FormatInt(noteIdAsInt, 10), "application/json", bytes.NewBuffer(jsonValue)) + resp, err := sendPutRequest(client, server.URL+paths.NoteCategoryApi+"?id="+strconv.FormatInt(noteIdAsInt, 10), "application/json", bytes.NewBuffer(jsonValue)) test_util.Ok(t, err) test_util.Equals(t, http.StatusCreated, resp.StatusCode) } @@ -174,7 +174,7 @@ func printBody(resp *http.Response) { type DiyMockDataStore struct { Func_StoreNewNote func(*models.Note) (models.NoteId, error) - Func_StoreNewNoteCategoryRelationship func(models.NoteId, models.Category) error + Func_StoreNewNoteCategoryRelationship func(models.NoteId, models.NoteCategory) error Func_StoreNewUser func(string, *models.EmailAddress, string) error Func_AuthenticateUserCredentials func(*models.EmailAddress, string) error Func_GetIdForUserWithEmailAddress func(*models.EmailAddress) (models.UserId, error) @@ -184,7 +184,7 @@ func (mock *DiyMockDataStore) StoreNewNote(note *models.Note) (models.NoteId, er return mock.Func_StoreNewNote(note) } -func (mock *DiyMockDataStore) StoreNewNoteCategoryRelationship(noteId models.NoteId, cat models.Category) error { +func (mock *DiyMockDataStore) StoreNewNoteCategoryRelationship(noteId models.NoteId, cat models.NoteCategory) error { return mock.Func_StoreNewNoteCategoryRelationship(noteId, cat) } diff --git a/models/datastore.go b/models/datastore.go index 88d7a4b..d76600e 100644 --- a/models/datastore.go +++ b/models/datastore.go @@ -32,7 +32,7 @@ func ConnectToDatabase(databaseUrl string) (*DB, error) { type Datastore interface { StoreNewNote(*Note) (NoteId, error) - StoreNewNoteCategoryRelationship(NoteId, Category) error + StoreNewNoteCategoryRelationship(NoteId, NoteCategory) error StoreNewUser(string, *EmailAddress, string) error AuthenticateUserCredentials(*EmailAddress, string) error GetIdForUserWithEmailAddress(*EmailAddress) (UserId, error) diff --git a/models/category.go b/models/note_category.go similarity index 67% rename from models/category.go rename to models/note_category.go index 733103f..e7992c9 100644 --- a/models/category.go +++ b/models/note_category.go @@ -4,10 +4,10 @@ import ( "errors" ) -type Category int +type NoteCategory int const ( - MARGINALIA Category = iota + MARGINALIA NoteCategory = iota META QUESTIONS PREDICTIONS @@ -20,18 +20,18 @@ var categoryStrings = [...]string{ "predictions", } -var CannotDeserializeCategoryStringError = errors.New("String does not correspond to a Note Category") +var CannotDeserializeNoteCategoryStringError = errors.New("String does not correspond to a Note Category") -func DeserializeCategory(input string) (Category, error) { +func DeserializeNoteCategory(input string) (NoteCategory, error) { for i := 0; i < len(categoryStrings); i++ { if input == categoryStrings[i] { - return Category(i), nil + return NoteCategory(i), nil } } - return 0, CannotDeserializeCategoryStringError + return 0, CannotDeserializeNoteCategoryStringError } -func (category Category) String() string { +func (category NoteCategory) String() string { if category < MARGINALIA || category > PREDICTIONS { return "Unknown" @@ -42,7 +42,7 @@ func (category Category) String() string { func (db *DB) StoreNewNoteCategoryRelationship( noteId NoteId, - category Category, + category NoteCategory, ) error { sqlQuery := ` INSERT INTO note_to_category_relationship (note_id, category) diff --git a/models/note_test.go b/models/note_test.go index 397d4d1..94003e0 100644 --- a/models/note_test.go +++ b/models/note_test.go @@ -7,7 +7,7 @@ import ( "github.com/atmiguel/cerealnotes/test_util" ) -var deserializationTests = []models.Category{ +var deserializationTests = []models.NoteCategory{ models.MARGINALIA, models.META, models.QUESTIONS, @@ -17,7 +17,7 @@ var deserializationTests = []models.Category{ func TestDeserialization(t *testing.T) { for _, val := range deserializationTests { t.Run(val.String(), func(t *testing.T) { - cat, err := models.DeserializeCategory(val.String()) + cat, err := models.DeserializeNoteCategory(val.String()) test_util.Ok(t, err) test_util.Equals(t, val, cat) }) diff --git a/paths/paths.go b/paths/paths.go index a36e22b..e9db54b 100644 --- a/paths/paths.go +++ b/paths/paths.go @@ -8,8 +8,8 @@ const ( HomePage = "/home" NotesPage = "/notes" - UserApi = "/api/user" - SessionApi = "/api/session" - NoteApi = "/api/note" - CategoryApi = "/api/note-category" + UserApi = "/api/user" + SessionApi = "/api/session" + NoteApi = "/api/note" + NoteCategoryApi = "/api/note-category" ) diff --git a/routers/routers.go b/routers/routers.go index e85ec20..ff12b73 100644 --- a/routers/routers.go +++ b/routers/routers.go @@ -70,7 +70,7 @@ func DefineRoutes(env *handlers.Environment) http.Handler { mux.handleUnAutheticedRequest(env, paths.SessionApi, handlers.HandleSessionApiRequest) mux.handleAuthenticatedApi(env, paths.NoteApi, handlers.HandleNoteApiRequest) - mux.handleAuthenticatedApi(env, paths.CategoryApi, handlers.HandleNoteCateogryApiRequest) + mux.handleAuthenticatedApi(env, paths.NoteCategoryApi, handlers.HandleNoteCateogryApiRequest) return mux }