From 133fb9dd743ed3aff5b31ad10f7b810958678152 Mon Sep 17 00:00:00 2001 From: G Date: Mon, 27 Aug 2018 12:01:45 -0700 Subject: [PATCH 01/43] rename --- migrations/0000_createDbs.sql | 6 +++--- {tools => migrations/tools}/drop_everything.sql | 2 +- models/note.go | 12 ++++++------ 3 files changed, 10 insertions(+), 10 deletions(-) rename {tools => migrations/tools}/drop_everything.sql (86%) diff --git a/migrations/0000_createDbs.sql b/migrations/0000_createDbs.sql index d418d6a..8f12770 100644 --- a/migrations/0000_createDbs.sql +++ b/migrations/0000_createDbs.sql @@ -1,5 +1,5 @@ -- Types -CREATE TYPE note_type AS ENUM ('predictions', 'marginalia', 'meta', 'questions'); +CREATE TYPE category AS ENUM ('predictions', 'marginalia', 'meta', 'questions'); -- Tables CREATE TABLE IF NOT EXISTS app_user ( @@ -29,7 +29,7 @@ CREATE TABLE IF NOT EXISTS note_to_publication_relationship ( publication_id bigint references publication(id) NOT NULL ); -CREATE TABLE IF NOT EXISTS note_to_type_relationship ( +CREATE TABLE IF NOT EXISTS note_to_category_relationship ( note_id bigint PRIMARY KEY references note(id), - type note_type NOT NULL + type category NOT NULL ); \ No newline at end of file diff --git a/tools/drop_everything.sql b/migrations/tools/drop_everything.sql similarity index 86% rename from tools/drop_everything.sql rename to migrations/tools/drop_everything.sql index babfd3f..b7b2cb1 100644 --- a/tools/drop_everything.sql +++ b/migrations/tools/drop_everything.sql @@ -1,4 +1,4 @@ -DROP TYPE note_type CASCADE; +DROP TYPE category CASCADE; DROP TABLE app_user CASCADE; diff --git a/models/note.go b/models/note.go index 255a250..a41cd42 100644 --- a/models/note.go +++ b/models/note.go @@ -6,28 +6,28 @@ import ( type NoteId int64 -type NoteType int +type Category int const ( - MARGINALIA NoteType = iota + MARGINALIA Category = iota META QUESTIONS PREDICTIONS ) -var noteTypeStrings = [...]string{ +var categoryStrings = [...]string{ "marginalia", "meta", "questions", "predictions", } -func (noteType NoteType) String() string { - if noteType < MARGINALIA || noteType > PREDICTIONS { +func (category Category) String() string { + if category < MARGINALIA || category > PREDICTIONS { return "Unknown" } - return noteTypeStrings[noteType] + return categoryStrings[category] } type Note struct { From e7ffc8e299f8518a29239067d27a22160f0d8089 Mon Sep 17 00:00:00 2001 From: G Date: Mon, 27 Aug 2018 12:52:45 -0700 Subject: [PATCH 02/43] compiles not tested --- databaseutil/databaseutil.go | 21 +++++++++++++ handlers/handlers.go | 48 ++++++++++++++++++++++++++++- models/note.go | 23 ++++++++++++-- services/noteservice/noteservice.go | 27 ++++++++++++++++ 4 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 services/noteservice/noteservice.go diff --git a/databaseutil/databaseutil.go b/databaseutil/databaseutil.go index bae1fb9..e1a9052 100644 --- a/databaseutil/databaseutil.go +++ b/databaseutil/databaseutil.go @@ -10,6 +10,7 @@ import ( "errors" "time" + // "github.com/atmiguel/cerealnotes/models" "github.com/lib/pq" ) @@ -94,6 +95,26 @@ func GetPasswordForUserWithEmailAddress(emailAddress string) ([]byte, error) { return password, nil } +func StoreNewNote(authorId int64, content string, creationTime time.Time) error { + sqlQuery := ` + INSERT INTO notes (author_id, content, creation_time) + VALUES ($1, $2, $3, $4, $5)` + + rows, err := db.Query(sqlQuery, authorId, content, creationTime) + if err != nil { + return convertPostgresError(err) + } + defer rows.Close() + + if err := rows.Err(); err != nil { + return convertPostgresError(err) + } + + // Todo update note to contain the new note id + + return nil +} + func GetIdForUserWithEmailAddress(emailAddress string) (int64, error) { sqlQuery := ` SELECT id FROM app_user diff --git a/handlers/handlers.go b/handlers/handlers.go index 8388d66..37b19b5 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -10,6 +10,7 @@ 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" ) @@ -246,8 +247,53 @@ func HandleNoteApiRequest( fmt.Fprint(responseWriter, string(notesInJson)) + case http.MethodPost: + type NoteForm struct { + Content string `json:"content"` + Category string `json:"category"` + } + + noteForm := new(NoteForm) + + if err := json.NewDecoder(request.Body).Decode(noteForm); err != nil { + http.Error(responseWriter, err.Error(), http.StatusBadRequest) + return + } + + if len(strings.TrimSpace(noteForm.Content)) == 0 { + http.Error(responseWriter, "Note content cannot be empty or just whitespace", http.StatusBadRequest) + return + } + + note := models.CreateNewNote(userId, noteForm.Content) + + if err := noteservice.StoreNewNote(note); err != nil { + http.Error(responseWriter, err.Error(), http.StatusInternalServerError) + return + } + + if !(noteForm.Category == "") { + + category, err := models.DeserializeCategory(noteForm.Category) + + if err != nil { + http.Error(responseWriter, err.Error(), http.StatusBadRequest) + return + } + + if err := noteservice.StoreNoteCategoryRelationship(note, category); err != nil { + http.Error(responseWriter, err.Error(), http.StatusInternalServerError) + return + } + + } + + statusCode := http.StatusCreated + + responseWriter.WriteHeader(statusCode) + default: - respondWithMethodNotAllowed(responseWriter, http.MethodGet) + respondWithMethodNotAllowed(responseWriter, http.MethodGet, http.MethodPost) } } diff --git a/models/note.go b/models/note.go index a41cd42..404d84f 100644 --- a/models/note.go +++ b/models/note.go @@ -1,6 +1,8 @@ package models import ( + "errors" + "strings" "time" ) @@ -22,11 +24,18 @@ var categoryStrings = [...]string{ "predictions", } -func (category Category) String() string { - if category < MARGINALIA || category > PREDICTIONS { - return "Unknown" +var UnDeserializeableCategoryStringError = errors.New("String does not correspond to a Note Category") + +func DeserializeCategory(input string) (Category, error) { + for i := 0; i < len(categoryStrings); i++ { + if strings.ToLower(input) == strings.ToLower(categoryStrings[i]) { + return Category(i), nil + } } + return MARGINALIA, UnDeserializeableCategoryStringError +} +func (category Category) String() string { return categoryStrings[category] } @@ -35,3 +44,11 @@ type Note struct { Content string `json:"content"` CreationTime time.Time `json:"creationTime"` } + +func CreateNewNote(userId UserId, content string) *Note { + return &Note{ + AuthorId: userId, + Content: content, + CreationTime: time.Now().UTC(), + } +} diff --git a/services/noteservice/noteservice.go b/services/noteservice/noteservice.go new file mode 100644 index 0000000..7bd49a1 --- /dev/null +++ b/services/noteservice/noteservice.go @@ -0,0 +1,27 @@ +/* +Package noteservice handles interactions with database layer. +*/ +package noteservice + +import ( + "errors" + + "github.com/atmiguel/cerealnotes/databaseutil" + "github.com/atmiguel/cerealnotes/models" +) + +var NoteIdIsNotValid error = errors.New("Email address already in use") + +func StoreNewNote( + note *models.Note, +) error { + databaseutil.StoreNewNote(int64(note.AuthorId), note.Content, note.CreationTime) + return nil +} + +func StoreNoteCategoryRelationship( + note *models.Note, + category models.Category, +) error { + return nil +} From ef2f17bedd314b3ca227d2e233ee77a31f8fb99c Mon Sep 17 00:00:00 2001 From: G Date: Mon, 27 Aug 2018 13:09:43 -0700 Subject: [PATCH 03/43] compiled but not tested --- databaseutil/databaseutil.go | 32 ++++++++++++++++++++++++----- handlers/handlers.go | 2 ++ models/note.go | 2 ++ services/noteservice/noteservice.go | 24 ++++++++++++++++++++-- 4 files changed, 53 insertions(+), 7 deletions(-) diff --git a/databaseutil/databaseutil.go b/databaseutil/databaseutil.go index e1a9052..8d315c2 100644 --- a/databaseutil/databaseutil.go +++ b/databaseutil/databaseutil.go @@ -95,22 +95,44 @@ func GetPasswordForUserWithEmailAddress(emailAddress string) ([]byte, error) { return password, nil } -func StoreNewNote(authorId int64, content string, creationTime time.Time) error { +func StoreNewNote(authorId int64, content string, creationTime time.Time) (int64, error) { sqlQuery := ` - INSERT INTO notes (author_id, content, creation_time) - VALUES ($1, $2, $3, $4, $5)` + INSERT INTO note (author_id, content, creation_time) + VALUES ($1, $2, $3, $4, $5) + RETURNING id` rows, err := db.Query(sqlQuery, authorId, content, creationTime) if err != nil { - return convertPostgresError(err) + return -1, convertPostgresError(err) } defer rows.Close() if err := rows.Err(); err != nil { + return -1, convertPostgresError(err) + } + + var lastInsertId int64 + if err := rows.Scan(&lastInsertId); err != nil { + return -1, convertPostgresError(err) + } + + return lastInsertId, nil +} + +func StoreNoteCategoryRelationship(noteId int64, category string) error { + sqlQuery := ` + INSERT INTO note_to_category_relationship (note_id, type) + VALUES ($1, $2)` + + rows, err := db.Query(sqlQuery, noteId, category) + if err != nil { return convertPostgresError(err) } + defer rows.Close() - // Todo update note to contain the new note id + if err := rows.Err(); err != nil { + return convertPostgresError(err) + } return nil } diff --git a/handlers/handlers.go b/handlers/handlers.go index 37b19b5..836b8f1 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -223,12 +223,14 @@ func HandleNoteApiRequest( switch request.Method { case http.MethodGet: note1 := &models.Note{ + Id: 1, AuthorId: 1, Content: "This is an example note.", CreationTime: time.Now().Add(-oneWeek).UTC(), } note2 := &models.Note{ + Id: 2, AuthorId: 2, Content: "What is this site for?", CreationTime: time.Now().Add(-60 * 12).UTC(), diff --git a/models/note.go b/models/note.go index 404d84f..a7baaf4 100644 --- a/models/note.go +++ b/models/note.go @@ -40,6 +40,7 @@ func (category Category) String() string { } type Note struct { + Id int64 `json:"authorId"` AuthorId UserId `json:"authorId"` Content string `json:"content"` CreationTime time.Time `json:"creationTime"` @@ -47,6 +48,7 @@ type Note struct { func CreateNewNote(userId UserId, content string) *Note { return &Note{ + Id: -1, AuthorId: userId, Content: content, CreationTime: time.Now().UTC(), diff --git a/services/noteservice/noteservice.go b/services/noteservice/noteservice.go index 7bd49a1..8278f7a 100644 --- a/services/noteservice/noteservice.go +++ b/services/noteservice/noteservice.go @@ -10,12 +10,23 @@ import ( "github.com/atmiguel/cerealnotes/models" ) -var NoteIdIsNotValid error = errors.New("Email address already in use") +var NoteIdNotSet error = errors.New("The NoteId was not set") func StoreNewNote( note *models.Note, ) error { - databaseutil.StoreNewNote(int64(note.AuthorId), note.Content, note.CreationTime) + + id, err := databaseutil.StoreNewNote(int64(note.AuthorId), note.Content, note.CreationTime) + if err != nil { + return err + } + + note.Id = id + + if note.Id < 0 { + return NoteIdNotSet + } + return nil } @@ -23,5 +34,14 @@ func StoreNoteCategoryRelationship( note *models.Note, category models.Category, ) error { + + if note.Id < 0 { + return NoteIdNotSet + } + + if err := databaseutil.StoreNoteCategoryRelationship(int64(note.Id), category.String()); err != nil { + return err + } + return nil } From 4f72f9a0ba874beb08918cd7f6256942b4d9689d Mon Sep 17 00:00:00 2001 From: G Date: Mon, 27 Aug 2018 13:26:15 -0700 Subject: [PATCH 04/43] fixed some runtime errors --- databaseutil/databaseutil.go | 21 ++++++++++++++++----- models/note.go | 2 +- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/databaseutil/databaseutil.go b/databaseutil/databaseutil.go index 8d315c2..aec9729 100644 --- a/databaseutil/databaseutil.go +++ b/databaseutil/databaseutil.go @@ -98,7 +98,7 @@ func GetPasswordForUserWithEmailAddress(emailAddress string) ([]byte, error) { func StoreNewNote(authorId int64, content string, creationTime time.Time) (int64, error) { sqlQuery := ` INSERT INTO note (author_id, content, creation_time) - VALUES ($1, $2, $3, $4, $5) + VALUES ($1, $2, $3) RETURNING id` rows, err := db.Query(sqlQuery, authorId, content, creationTime) @@ -107,12 +107,23 @@ func StoreNewNote(authorId int64, content string, creationTime time.Time) (int64 } defer rows.Close() - if err := rows.Err(); err != nil { - return -1, convertPostgresError(err) + var lastInsertId int64 + for rows.Next() { + + if lastInsertId != 0 { + return -1, QueryResultContainedMultipleRowsError + } + + if err := rows.Scan(&lastInsertId); err != nil { + return -1, convertPostgresError(err) + } } - var lastInsertId int64 - if err := rows.Scan(&lastInsertId); err != nil { + if lastInsertId == 0 { + return -1, QueryResultContainedNoRowsError + } + + if err := rows.Err(); err != nil { return -1, convertPostgresError(err) } diff --git a/models/note.go b/models/note.go index a7baaf4..2cd9bd9 100644 --- a/models/note.go +++ b/models/note.go @@ -40,7 +40,7 @@ func (category Category) String() string { } type Note struct { - Id int64 `json:"authorId"` + Id int64 `json:"id"` AuthorId UserId `json:"authorId"` Content string `json:"content"` CreationTime time.Time `json:"creationTime"` From 0b205df6f1f0d6af11714a9bd86fa4390cd7077c Mon Sep 17 00:00:00 2001 From: G Date: Mon, 27 Aug 2018 13:35:07 -0700 Subject: [PATCH 05/43] addressed adrians comments from previous pull request --- handlers/handlers.go | 6 ++---- models/note.go | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index 836b8f1..082e8fb 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -276,7 +276,7 @@ func HandleNoteApiRequest( if !(noteForm.Category == "") { - category, err := models.DeserializeCategory(noteForm.Category) + category, err := models.DeserializeCategory(string.ToLower(noteForm.Category)) if err != nil { http.Error(responseWriter, err.Error(), http.StatusBadRequest) @@ -290,9 +290,7 @@ func HandleNoteApiRequest( } - statusCode := http.StatusCreated - - responseWriter.WriteHeader(statusCode) + responseWriter.WriteHeader(http.StatusCreated) default: respondWithMethodNotAllowed(responseWriter, http.MethodGet, http.MethodPost) diff --git a/models/note.go b/models/note.go index 2cd9bd9..890255d 100644 --- a/models/note.go +++ b/models/note.go @@ -28,7 +28,7 @@ var UnDeserializeableCategoryStringError = errors.New("String does not correspon func DeserializeCategory(input string) (Category, error) { for i := 0; i < len(categoryStrings); i++ { - if strings.ToLower(input) == strings.ToLower(categoryStrings[i]) { + if input == categoryStrings[i] { return Category(i), nil } } From 95e07c57d9e98cff0e18031a63e75de4b549a063 Mon Sep 17 00:00:00 2001 From: G Date: Mon, 27 Aug 2018 13:36:31 -0700 Subject: [PATCH 06/43] removed commented out package --- databaseutil/databaseutil.go | 1 - 1 file changed, 1 deletion(-) diff --git a/databaseutil/databaseutil.go b/databaseutil/databaseutil.go index aec9729..210f4e1 100644 --- a/databaseutil/databaseutil.go +++ b/databaseutil/databaseutil.go @@ -10,7 +10,6 @@ import ( "errors" "time" - // "github.com/atmiguel/cerealnotes/models" "github.com/lib/pq" ) From ffabd4b21cda3b88ca6c8f8eb2af34172e40f54f Mon Sep 17 00:00:00 2001 From: G Date: Wed, 29 Aug 2018 10:04:44 -0700 Subject: [PATCH 07/43] typos --- handlers/handlers.go | 2 +- models/note.go | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index 082e8fb..d853a63 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -276,7 +276,7 @@ func HandleNoteApiRequest( if !(noteForm.Category == "") { - category, err := models.DeserializeCategory(string.ToLower(noteForm.Category)) + category, err := models.DeserializeCategory(strings.ToLower(noteForm.Category)) if err != nil { http.Error(responseWriter, err.Error(), http.StatusBadRequest) diff --git a/models/note.go b/models/note.go index 890255d..eb4b287 100644 --- a/models/note.go +++ b/models/note.go @@ -2,7 +2,6 @@ package models import ( "errors" - "strings" "time" ) From 003ec3984948e2939251020238a3a1dc618329e3 Mon Sep 17 00:00:00 2001 From: G Date: Thu, 13 Sep 2018 09:21:16 -0700 Subject: [PATCH 08/43] renamed database column --- migrations/0000_createDbs.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/0000_createDbs.sql b/migrations/0000_createDbs.sql index 8f12770..1c4294d 100644 --- a/migrations/0000_createDbs.sql +++ b/migrations/0000_createDbs.sql @@ -31,5 +31,5 @@ CREATE TABLE IF NOT EXISTS note_to_publication_relationship ( CREATE TABLE IF NOT EXISTS note_to_category_relationship ( note_id bigint PRIMARY KEY references note(id), - type category NOT NULL + category_type category NOT NULL ); \ No newline at end of file From 6fd0543280870aea045716c8f568ab694cdba08b Mon Sep 17 00:00:00 2001 From: G Date: Thu, 13 Sep 2018 09:52:48 -0700 Subject: [PATCH 09/43] addressed some comments --- databaseutil/databaseutil.go | 24 ++++++++++++------------ handlers/handlers.go | 4 ++-- models/note.go | 4 ++-- services/noteservice/noteservice.go | 6 +++--- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/databaseutil/databaseutil.go b/databaseutil/databaseutil.go index 210f4e1..01a1f9c 100644 --- a/databaseutil/databaseutil.go +++ b/databaseutil/databaseutil.go @@ -94,7 +94,7 @@ func GetPasswordForUserWithEmailAddress(emailAddress string) ([]byte, error) { return password, nil } -func StoreNewNote(authorId int64, content string, creationTime time.Time) (int64, error) { +func InsertNewNote(authorId int64, content string, creationTime time.Time) (int64, error) { sqlQuery := ` INSERT INTO note (author_id, content, creation_time) VALUES ($1, $2, $3) @@ -102,34 +102,34 @@ func StoreNewNote(authorId int64, content string, creationTime time.Time) (int64 rows, err := db.Query(sqlQuery, authorId, content, creationTime) if err != nil { - return -1, convertPostgresError(err) + return 0, convertPostgresError(err) } defer rows.Close() - var lastInsertId int64 + var noteId int64 = 0 for rows.Next() { - if lastInsertId != 0 { - return -1, QueryResultContainedMultipleRowsError + if noteId != 0 { + return 0, QueryResultContainedMultipleRowsError } - if err := rows.Scan(&lastInsertId); err != nil { - return -1, convertPostgresError(err) + if err := rows.Scan(¬eId); err != nil { + return 0, convertPostgresError(err) } } - if lastInsertId == 0 { - return -1, QueryResultContainedNoRowsError + if noteId == 0 { + return 0, QueryResultContainedNoRowsError } if err := rows.Err(); err != nil { - return -1, convertPostgresError(err) + return 0, convertPostgresError(err) } - return lastInsertId, nil + return noteId, nil } -func StoreNoteCategoryRelationship(noteId int64, category string) error { +func InsertNoteCategoryRelationship(noteId int64, category string) error { sqlQuery := ` INSERT INTO note_to_category_relationship (note_id, type) VALUES ($1, $2)` diff --git a/handlers/handlers.go b/handlers/handlers.go index d853a63..3808737 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -274,7 +274,7 @@ func HandleNoteApiRequest( return } - if !(noteForm.Category == "") { + if noteForm.Category != "" { category, err := models.DeserializeCategory(strings.ToLower(noteForm.Category)) @@ -283,7 +283,7 @@ func HandleNoteApiRequest( return } - if err := noteservice.StoreNoteCategoryRelationship(note, category); err != nil { + if err := noteservice.StoreNewNoteCategoryRelationship(note, category); err != nil { http.Error(responseWriter, err.Error(), http.StatusInternalServerError) return } diff --git a/models/note.go b/models/note.go index eb4b287..49d364a 100644 --- a/models/note.go +++ b/models/note.go @@ -23,7 +23,7 @@ var categoryStrings = [...]string{ "predictions", } -var UnDeserializeableCategoryStringError = errors.New("String does not correspond to a Note Category") +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++ { @@ -31,7 +31,7 @@ func DeserializeCategory(input string) (Category, error) { return Category(i), nil } } - return MARGINALIA, UnDeserializeableCategoryStringError + return MARGINALIA, CannotDeserializeCategoryStringError } func (category Category) String() string { diff --git a/services/noteservice/noteservice.go b/services/noteservice/noteservice.go index 8278f7a..cb8d75f 100644 --- a/services/noteservice/noteservice.go +++ b/services/noteservice/noteservice.go @@ -16,7 +16,7 @@ func StoreNewNote( note *models.Note, ) error { - id, err := databaseutil.StoreNewNote(int64(note.AuthorId), note.Content, note.CreationTime) + id, err := databaseutil.InsertNewNote(int64(note.AuthorId), note.Content, note.CreationTime) if err != nil { return err } @@ -30,7 +30,7 @@ func StoreNewNote( return nil } -func StoreNoteCategoryRelationship( +func StoreNewNoteCategoryRelationship( note *models.Note, category models.Category, ) error { @@ -39,7 +39,7 @@ func StoreNoteCategoryRelationship( return NoteIdNotSet } - if err := databaseutil.StoreNoteCategoryRelationship(int64(note.Id), category.String()); err != nil { + if err := databaseutil.InsertNoteCategoryRelationship(int64(note.Id), category.String()); err != nil { return err } From 613dc9f94745a945625e2ff528a98cf1724c4844 Mon Sep 17 00:00:00 2001 From: G Date: Thu, 13 Sep 2018 19:35:22 -0700 Subject: [PATCH 10/43] no noteId -> mapping --- handlers/handlers.go | 24 +++++++++++-------- models/note.go | 10 -------- services/noteservice/noteservice.go | 37 +++++++++++++++-------------- 3 files changed, 33 insertions(+), 38 deletions(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index 3808737..f88031c 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -222,23 +222,22 @@ func HandleNoteApiRequest( ) { switch request.Method { case http.MethodGet: - note1 := &models.Note{ - Id: 1, + + var noteMap noteservice.NoteMap = make(map[models.NoteId]*models.Note, 2) + + noteMap[models.NoteId(1)] = &models.Note{ AuthorId: 1, Content: "This is an example note.", CreationTime: time.Now().Add(-oneWeek).UTC(), } - note2 := &models.Note{ - Id: 2, + noteMap[models.NoteId(2)] = &models.Note{ AuthorId: 2, Content: "What is this site for?", CreationTime: time.Now().Add(-60 * 12).UTC(), } - notes := [2]*models.Note{note1, note2} - - notesInJson, err := json.Marshal(notes) + notesInJson, err := noteMap.ToJson() if err != nil { http.Error(responseWriter, err.Error(), http.StatusInternalServerError) return @@ -267,9 +266,14 @@ func HandleNoteApiRequest( return } - note := models.CreateNewNote(userId, noteForm.Content) + note := &models.Note{ + AuthorId: models.UserId(userId), + Content: noteForm.Content, + CreationTime: time.Now().UTC(), + } - if err := noteservice.StoreNewNote(note); err != nil { + noteId, err := noteservice.StoreNewNote(note) + if err != nil { http.Error(responseWriter, err.Error(), http.StatusInternalServerError) return } @@ -283,7 +287,7 @@ func HandleNoteApiRequest( return } - if err := noteservice.StoreNewNoteCategoryRelationship(note, category); err != nil { + if err := noteservice.StoreNewNoteCategoryRelationship(noteId, category); err != nil { http.Error(responseWriter, err.Error(), http.StatusInternalServerError) return } diff --git a/models/note.go b/models/note.go index 49d364a..a03df84 100644 --- a/models/note.go +++ b/models/note.go @@ -39,17 +39,7 @@ func (category Category) String() string { } type Note struct { - Id int64 `json:"id"` AuthorId UserId `json:"authorId"` Content string `json:"content"` CreationTime time.Time `json:"creationTime"` } - -func CreateNewNote(userId UserId, content string) *Note { - return &Note{ - Id: -1, - AuthorId: userId, - Content: content, - CreationTime: time.Now().UTC(), - } -} diff --git a/services/noteservice/noteservice.go b/services/noteservice/noteservice.go index cb8d75f..1b51cfa 100644 --- a/services/noteservice/noteservice.go +++ b/services/noteservice/noteservice.go @@ -4,44 +4,45 @@ Package noteservice handles interactions with database layer. package noteservice import ( - "errors" + "encoding/json" + "fmt" "github.com/atmiguel/cerealnotes/databaseutil" "github.com/atmiguel/cerealnotes/models" ) -var NoteIdNotSet error = errors.New("The NoteId was not set") - func StoreNewNote( note *models.Note, -) error { +) (models.NoteId, error) { id, err := databaseutil.InsertNewNote(int64(note.AuthorId), note.Content, note.CreationTime) if err != nil { - return err + return models.NoteId(0), err } - note.Id = id + return models.NoteId(id), nil +} - if note.Id < 0 { - return NoteIdNotSet +func StoreNewNoteCategoryRelationship( + noteId models.NoteId, + category models.Category, +) error { + if err := databaseutil.InsertNoteCategoryRelationship(int64(noteId), category.String()); err != nil { + return err } return nil } -func StoreNewNoteCategoryRelationship( - note *models.Note, - category models.Category, -) error { +type NoteMap map[models.NoteId]*models.Note - if note.Id < 0 { - return NoteIdNotSet - } +func (noteMap NoteMap) ToJson() ([]byte, error) { + // json doesn't support int indexed maps + datas := make(map[string]models.Note, len(noteMap)) - if err := databaseutil.InsertNoteCategoryRelationship(int64(note.Id), category.String()); err != nil { - return err + for id, note := range noteMap { + datas[fmt.Sprint(id)] = *note } - return nil + return json.Marshal(datas) } From cc0c32b116291cbf76cb7e9694ed8ef54c663993 Mon Sep 17 00:00:00 2001 From: G Date: Thu, 13 Sep 2018 19:56:09 -0700 Subject: [PATCH 11/43] figured out how to 'extend' classes --- routers/routers.go | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/routers/routers.go b/routers/routers.go index 6bcd2b5..49480f1 100644 --- a/routers/routers.go +++ b/routers/routers.go @@ -10,9 +10,20 @@ import ( "github.com/atmiguel/cerealnotes/paths" ) +type routeHandler struct { + *http.ServeMux +} + +func (mux *routeHandler) handleAuthenticated( + pattern string, + handlerFunc handlers.AuthentictedRequestHandlerType, +) { + mux.HandleFunc(pattern, handlers.AuthenticateOrRedirectToLogin(handlerFunc)) +} + // DefineRoutes returns a new servemux with all the required path and handler pairs attached. func DefineRoutes() http.Handler { - mux := http.NewServeMux() + mux := &routeHandler{http.NewServeMux()} // static files { staticDirectoryName := "static" @@ -32,23 +43,15 @@ func DefineRoutes() http.Handler { // pages mux.HandleFunc(paths.LoginOrSignupPage, handlers.HandleLoginOrSignupPageRequest) - handleAuthenticated(mux, paths.HomePage, handlers.HandleHomePageRequest) - handleAuthenticated(mux, paths.NotesPage, handlers.HandleNotesPageRequest) + mux.handleAuthenticated(paths.HomePage, handlers.HandleHomePageRequest) + mux.handleAuthenticated(paths.NotesPage, handlers.HandleNotesPageRequest) // api mux.HandleFunc(paths.UserApi, handlers.HandleUserApiRequest) mux.HandleFunc(paths.SessionApi, handlers.HandleSessionApiRequest) - handleAuthenticated(mux, paths.NoteApi, handlers.HandleNoteApiRequest) + mux.handleAuthenticated(paths.NoteApi, handlers.HandleNoteApiRequest) return mux } - -func handleAuthenticated( - mux *http.ServeMux, - pattern string, - handlerFunc handlers.AuthentictedRequestHandlerType, -) { - mux.HandleFunc(pattern, handlers.AuthenticateOrRedirectToLogin(handlerFunc)) -} From a830edd648f133750a6e976fd2e0ff986b2992e6 Mon Sep 17 00:00:00 2001 From: G Date: Thu, 13 Sep 2018 20:10:25 -0700 Subject: [PATCH 12/43] handle unauthorized redirect a bit better --- handlers/handlers.go | 20 ++++++++++++++------ routers/routers.go | 2 +- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index 8388d66..4048ebe 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -256,17 +256,25 @@ type AuthentictedRequestHandlerType func( *http.Request, models.UserId) -func AuthenticateOrRedirectToLogin( +func AuthenticateOrRedirect( authenticatedHandlerFunc AuthentictedRequestHandlerType, + redirectPath string, ) http.HandlerFunc { return func(responseWriter http.ResponseWriter, request *http.Request) { if userId, err := getUserIdFromJwtToken(request); err != nil { + switch request.Method { // If not logged in, redirect to login page - http.Redirect( - responseWriter, - request, - paths.LoginOrSignupPage, - http.StatusTemporaryRedirect) + case http.MethodGet: + http.Redirect( + responseWriter, + request, + redirectPath, + http.StatusUnauthorized) + return + default: + http.Error(responseWriter, err.Error(), http.StatusUnauthorized) + return + } } else { authenticatedHandlerFunc(responseWriter, request, userId) } diff --git a/routers/routers.go b/routers/routers.go index 49480f1..6954650 100644 --- a/routers/routers.go +++ b/routers/routers.go @@ -18,7 +18,7 @@ func (mux *routeHandler) handleAuthenticated( pattern string, handlerFunc handlers.AuthentictedRequestHandlerType, ) { - mux.HandleFunc(pattern, handlers.AuthenticateOrRedirectToLogin(handlerFunc)) + mux.HandleFunc(pattern, handlers.AuthenticateOrRedirect(handlerFunc, paths.LoginOrSignupPage)) } // DefineRoutes returns a new servemux with all the required path and handler pairs attached. From 8d6ed421a70e33009ed1db0d4e0e2066741571bf Mon Sep 17 00:00:00 2001 From: G Date: Thu, 13 Sep 2018 20:22:38 -0700 Subject: [PATCH 13/43] for api return 404 or 301 on bad requests, rather than redirecting to home --- handlers/handlers.go | 17 ++++++++++++++--- routers/routers.go | 10 +++++++++- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index 4048ebe..8412ef5 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -269,11 +269,10 @@ func AuthenticateOrRedirect( responseWriter, request, redirectPath, - http.StatusUnauthorized) + http.StatusTemporaryRedirect) return default: - http.Error(responseWriter, err.Error(), http.StatusUnauthorized) - return + respondWithMethodNotAllowed(responseWriter, http.MethodGet) } } else { authenticatedHandlerFunc(responseWriter, request, userId) @@ -281,6 +280,18 @@ func AuthenticateOrRedirect( } } +func AuthenticateOrUnauthorized( + authenticatedHandlerFunc AuthentictedRequestHandlerType, +) http.HandlerFunc { + return func(responseWriter http.ResponseWriter, request *http.Request) { + if userId, err := getUserIdFromJwtToken(request); err != nil { + http.Error(responseWriter, err.Error(), http.StatusUnauthorized) + } else { + authenticatedHandlerFunc(responseWriter, request, userId) + } + } +} + func RedirectToPathHandler( path string, ) http.HandlerFunc { diff --git a/routers/routers.go b/routers/routers.go index 6954650..c22fc97 100644 --- a/routers/routers.go +++ b/routers/routers.go @@ -21,6 +21,13 @@ func (mux *routeHandler) handleAuthenticated( mux.HandleFunc(pattern, handlers.AuthenticateOrRedirect(handlerFunc, paths.LoginOrSignupPage)) } +func (mux *routeHandler) handleAuthenticatedApi( + pattern string, + handlerFunc handlers.AuthentictedRequestHandlerType, +) { + mux.HandleFunc(pattern, handlers.AuthenticateOrUnauthorized(handlerFunc)) +} + // DefineRoutes returns a new servemux with all the required path and handler pairs attached. func DefineRoutes() http.Handler { mux := &routeHandler{http.NewServeMux()} @@ -38,6 +45,7 @@ func DefineRoutes() http.Handler { // Redirects mux.HandleFunc("/", handlers.RedirectToPathHandler(paths.HomePage)) + mux.HandleFunc("/api/", http.NotFound) mux.HandleFunc("/favicon.ico", handlers.RedirectToPathHandler("/static/favicon.ico")) // pages @@ -51,7 +59,7 @@ func DefineRoutes() http.Handler { mux.HandleFunc(paths.UserApi, handlers.HandleUserApiRequest) mux.HandleFunc(paths.SessionApi, handlers.HandleSessionApiRequest) - mux.handleAuthenticated(paths.NoteApi, handlers.HandleNoteApiRequest) + mux.handleAuthenticatedApi(paths.NoteApi, handlers.HandleNoteApiRequest) return mux } From 38c36c8e856972939f04fd986dbca03b4355ffc8 Mon Sep 17 00:00:00 2001 From: G Date: Fri, 14 Sep 2018 10:06:56 -0700 Subject: [PATCH 14/43] addressed new comments --- handlers/handlers.go | 9 +++++---- migrations/0000_createDbs.sql | 4 ++-- migrations/tools/drop_everything.sql | 2 +- routers/routers.go | 12 ++++++------ 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index 8412ef5..1d05dec 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -251,13 +251,13 @@ func HandleNoteApiRequest( } } -type AuthentictedRequestHandlerType func( +type AuthenticatedRequestHandlerType func( http.ResponseWriter, *http.Request, models.UserId) func AuthenticateOrRedirect( - authenticatedHandlerFunc AuthentictedRequestHandlerType, + authenticatedHandlerFunc AuthenticatedRequestHandlerType, redirectPath string, ) http.HandlerFunc { return func(responseWriter http.ResponseWriter, request *http.Request) { @@ -280,11 +280,12 @@ func AuthenticateOrRedirect( } } -func AuthenticateOrUnauthorized( - authenticatedHandlerFunc AuthentictedRequestHandlerType, +func AuthenticateOrReturnUnauthorized( + authenticatedHandlerFunc AuthenticatedRequestHandlerType, ) http.HandlerFunc { return func(responseWriter http.ResponseWriter, request *http.Request) { if userId, err := getUserIdFromJwtToken(request); err != nil { + responseWriter.Header().Set("WWW-Authenticate", "Please log in to see this page") http.Error(responseWriter, err.Error(), http.StatusUnauthorized) } else { authenticatedHandlerFunc(responseWriter, request, userId) diff --git a/migrations/0000_createDbs.sql b/migrations/0000_createDbs.sql index 1c4294d..9774b6c 100644 --- a/migrations/0000_createDbs.sql +++ b/migrations/0000_createDbs.sql @@ -1,5 +1,5 @@ -- Types -CREATE TYPE category AS ENUM ('predictions', 'marginalia', 'meta', 'questions'); +CREATE TYPE category_type AS ENUM ('predictions', 'marginalia', 'meta', 'questions'); -- Tables CREATE TABLE IF NOT EXISTS app_user ( @@ -31,5 +31,5 @@ CREATE TABLE IF NOT EXISTS note_to_publication_relationship ( CREATE TABLE IF NOT EXISTS note_to_category_relationship ( note_id bigint PRIMARY KEY references note(id), - category_type category NOT NULL + category category_type NOT NULL ); \ No newline at end of file diff --git a/migrations/tools/drop_everything.sql b/migrations/tools/drop_everything.sql index b7b2cb1..ac4fd8a 100644 --- a/migrations/tools/drop_everything.sql +++ b/migrations/tools/drop_everything.sql @@ -1,4 +1,4 @@ -DROP TYPE category CASCADE; +DROP TYPE category_type CASCADE; DROP TABLE app_user CASCADE; diff --git a/routers/routers.go b/routers/routers.go index c22fc97..28b0165 100644 --- a/routers/routers.go +++ b/routers/routers.go @@ -14,18 +14,18 @@ type routeHandler struct { *http.ServeMux } -func (mux *routeHandler) handleAuthenticated( +func (mux *routeHandler) handleAuthenticatedPage( pattern string, - handlerFunc handlers.AuthentictedRequestHandlerType, + handlerFunc handlers.AuthenticatedRequestHandlerType, ) { mux.HandleFunc(pattern, handlers.AuthenticateOrRedirect(handlerFunc, paths.LoginOrSignupPage)) } func (mux *routeHandler) handleAuthenticatedApi( pattern string, - handlerFunc handlers.AuthentictedRequestHandlerType, + handlerFunc handlers.AuthenticatedRequestHandlerType, ) { - mux.HandleFunc(pattern, handlers.AuthenticateOrUnauthorized(handlerFunc)) + mux.HandleFunc(pattern, handlers.AuthenticateOrReturnUnauthorized(handlerFunc)) } // DefineRoutes returns a new servemux with all the required path and handler pairs attached. @@ -51,8 +51,8 @@ func DefineRoutes() http.Handler { // pages mux.HandleFunc(paths.LoginOrSignupPage, handlers.HandleLoginOrSignupPageRequest) - mux.handleAuthenticated(paths.HomePage, handlers.HandleHomePageRequest) - mux.handleAuthenticated(paths.NotesPage, handlers.HandleNotesPageRequest) + mux.handleAuthenticatedPage(paths.HomePage, handlers.HandleHomePageRequest) + mux.handleAuthenticatedPage(paths.NotesPage, handlers.HandleNotesPageRequest) // api From 451f2adae98a02411f31e5f916da1b7624ca6a87 Mon Sep 17 00:00:00 2001 From: G Date: Fri, 14 Sep 2018 11:28:15 -0700 Subject: [PATCH 15/43] fixed some issues --- handlers/handlers.go | 8 ++++---- models/note.go | 5 +++++ services/noteservice/noteservice.go | 6 +++--- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index f88031c..174d1ef 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -223,21 +223,21 @@ func HandleNoteApiRequest( switch request.Method { case http.MethodGet: - var noteMap noteservice.NoteMap = make(map[models.NoteId]*models.Note, 2) + var notesById noteservice.NoteMap = make(map[models.NoteId]*models.Note, 2) - noteMap[models.NoteId(1)] = &models.Note{ + notesById[models.NoteId(1)] = &models.Note{ AuthorId: 1, Content: "This is an example note.", CreationTime: time.Now().Add(-oneWeek).UTC(), } - noteMap[models.NoteId(2)] = &models.Note{ + notesById[models.NoteId(2)] = &models.Note{ AuthorId: 2, Content: "What is this site for?", CreationTime: time.Now().Add(-60 * 12).UTC(), } - notesInJson, err := noteMap.ToJson() + notesInJson, err := notesById.ToJson() if err != nil { http.Error(responseWriter, err.Error(), http.StatusInternalServerError) return diff --git a/models/note.go b/models/note.go index a03df84..8685a23 100644 --- a/models/note.go +++ b/models/note.go @@ -35,6 +35,11 @@ func DeserializeCategory(input string) (Category, error) { } func (category Category) String() string { + + if category < MARGINALIA || category > PREDICTIONS { + return "Unknown" + } + return categoryStrings[category] } diff --git a/services/noteservice/noteservice.go b/services/noteservice/noteservice.go index 1b51cfa..52a9b77 100644 --- a/services/noteservice/noteservice.go +++ b/services/noteservice/noteservice.go @@ -38,11 +38,11 @@ type NoteMap map[models.NoteId]*models.Note func (noteMap NoteMap) ToJson() ([]byte, error) { // json doesn't support int indexed maps - datas := make(map[string]models.Note, len(noteMap)) + notesByIdString := make(map[string]models.Note, len(noteMap)) for id, note := range noteMap { - datas[fmt.Sprint(id)] = *note + notesByIdString[fmt.Sprint(id)] = *note } - return json.Marshal(datas) + return json.Marshal(notesByIdString) } From 4a0c96624e1aa289e2eac43139e44efc877e364b Mon Sep 17 00:00:00 2001 From: G Date: Fri, 14 Sep 2018 12:25:40 -0700 Subject: [PATCH 16/43] merge --- databaseutil/databaseutil.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/databaseutil/databaseutil.go b/databaseutil/databaseutil.go index 01a1f9c..02e24cf 100644 --- a/databaseutil/databaseutil.go +++ b/databaseutil/databaseutil.go @@ -131,7 +131,7 @@ func InsertNewNote(authorId int64, content string, creationTime time.Time) (int6 func InsertNoteCategoryRelationship(noteId int64, category string) error { sqlQuery := ` - INSERT INTO note_to_category_relationship (note_id, type) + INSERT INTO note_to_category_relationship (note_id, category) VALUES ($1, $2)` rows, err := db.Query(sqlQuery, noteId, category) From 0217fddeb75fba2cb9517ce81f8fe9ce647bd944 Mon Sep 17 00:00:00 2001 From: G Date: Fri, 14 Sep 2018 14:38:37 -0700 Subject: [PATCH 17/43] was missing the type and realm parameter --- handlers/handlers.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index c31ece7..2c66684 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -334,8 +334,9 @@ func AuthenticateOrReturnUnauthorized( authenticatedHandlerFunc AuthenticatedRequestHandlerType, ) http.HandlerFunc { return func(responseWriter http.ResponseWriter, request *http.Request) { + if userId, err := getUserIdFromJwtToken(request); err != nil { - responseWriter.Header().Set("WWW-Authenticate", "Please log in to see this page") + responseWriter.Header().Set("WWW-Authenticate", `Bearer realm="`+request.URL.Path+`"`) http.Error(responseWriter, err.Error(), http.StatusUnauthorized) } else { authenticatedHandlerFunc(responseWriter, request, userId) From 3e5c59c534830e319919221292ac3a03c69d8eeb Mon Sep 17 00:00:00 2001 From: G Date: Sat, 15 Sep 2018 18:02:43 -0700 Subject: [PATCH 18/43] added category api --- handlers/handlers.go | 51 +++++++++++++++++++++++++++++++++----------- paths/paths.go | 7 +++--- routers/routers.go | 1 + 3 files changed, 43 insertions(+), 16 deletions(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index 2c66684..dd1a769 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -5,6 +5,7 @@ import ( "fmt" "html/template" "net/http" + "strconv" "strings" "time" @@ -250,8 +251,7 @@ func HandleNoteApiRequest( case http.MethodPost: type NoteForm struct { - Content string `json:"content"` - Category string `json:"category"` + Content string `json:"content"` } noteForm := new(NoteForm) @@ -278,27 +278,52 @@ func HandleNoteApiRequest( return } - if noteForm.Category != "" { + responseWriter.Write([]byte(`{NoteId: "` + strconv.FormatInt(int64(noteId), 10) + `"}`)) + responseWriter.WriteHeader(http.StatusCreated) - category, err := models.DeserializeCategory(strings.ToLower(noteForm.Category)) + default: + respondWithMethodNotAllowed(responseWriter, http.MethodGet, http.MethodPost) + } +} - if err != nil { - http.Error(responseWriter, err.Error(), http.StatusBadRequest) - return - } +func HandleCategoryApiRequest( + responseWriter http.ResponseWriter, + request *http.Request, + userId models.UserId, +) { + switch request.Method { + case http.MethodPost: - if err := noteservice.StoreNewNoteCategoryRelationship(noteId, category); err != nil { - http.Error(responseWriter, err.Error(), http.StatusInternalServerError) - return - } + type CategoryForm struct { + NoteId int64 `json:"noteId"` + Category string `json:"category"` + } + + noteForm := new(CategoryForm) + + if err := json.NewDecoder(request.Body).Decode(noteForm); err != nil { + http.Error(responseWriter, err.Error(), http.StatusBadRequest) + return + } + + category, err := models.DeserializeCategory(strings.ToLower(noteForm.Category)) + if err != nil { + http.Error(responseWriter, err.Error(), http.StatusBadRequest) + return + } + + if err := noteservice.StoreNewNoteCategoryRelationship(models.NoteId(noteForm.NoteId), category); err != nil { + http.Error(responseWriter, err.Error(), http.StatusInternalServerError) + return } responseWriter.WriteHeader(http.StatusCreated) default: - respondWithMethodNotAllowed(responseWriter, http.MethodGet, http.MethodPost) + respondWithMethodNotAllowed(responseWriter, http.MethodPost) } + } type AuthenticatedRequestHandlerType func( diff --git a/paths/paths.go b/paths/paths.go index 8db612c..fedf71a 100644 --- a/paths/paths.go +++ b/paths/paths.go @@ -8,7 +8,8 @@ const ( HomePage = "/home" NotesPage = "/notes" - UserApi = "/api/user" - SessionApi = "/api/session" - NoteApi = "/api/note" + UserApi = "/api/user" + SessionApi = "/api/session" + NoteApi = "/api/note" + CategoryApi = "/api/Category" ) diff --git a/routers/routers.go b/routers/routers.go index 28b0165..1c86168 100644 --- a/routers/routers.go +++ b/routers/routers.go @@ -60,6 +60,7 @@ func DefineRoutes() http.Handler { mux.HandleFunc(paths.SessionApi, handlers.HandleSessionApiRequest) mux.handleAuthenticatedApi(paths.NoteApi, handlers.HandleNoteApiRequest) + mux.handleAuthenticatedApi(paths.CategoryApi, handlers.HandleCategoryApiRequest) return mux } From 07a25f8a31319ed4145fe69f3fd2edf0120fe7da Mon Sep 17 00:00:00 2001 From: G Date: Sat, 15 Sep 2018 18:54:13 -0700 Subject: [PATCH 19/43] 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 20/43] 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 21/43] 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 22/43] 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 23/43] 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 24/43] 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 25/43] 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 26/43] 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 27/43] 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 28/43] 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 29/43] 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 30/43] 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 31/43] 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 32/43] 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 33/43] 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 34/43] 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 35/43] 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 36/43] 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 88d7f9d80a31d3c49a80667794be1a5a1f011879 Mon Sep 17 00:00:00 2001 From: G Date: Tue, 18 Sep 2018 11:36:47 -0700 Subject: [PATCH 37/43] database helper --- models/category.go | 14 ++++---- models/databasehelper.go | 75 ++++++++++++++++++++++++++++++++++++++++ models/datastore.go | 24 ------------- models/note.go | 26 ++------------ models/user.go | 52 ++++++---------------------- 5 files changed, 94 insertions(+), 97 deletions(-) create mode 100644 models/databasehelper.go diff --git a/models/category.go b/models/category.go index af4cce5..51708f9 100644 --- a/models/category.go +++ b/models/category.go @@ -21,6 +21,7 @@ var categoryStrings = [...]string{ } var CannotDeserializeCategoryStringError = errors.New("String does not correspond to a Note Category") +var NoteAlreadyContainsCategoryError = errors.New("NoteId already has a category stored for it") func DeserializeCategory(input string) (Category, error) { for i := 0; i < len(categoryStrings); i++ { @@ -48,14 +49,11 @@ func (db *DB) StoreNewNoteCategoryRelationship( 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) + if err := db.execNoResults(sqlQuery, int64(noteId), category.String()); err != nil { + if err == UniqueConstraintError { + return NoteAlreadyContainsCategoryError + } + return err } return nil diff --git a/models/databasehelper.go b/models/databasehelper.go new file mode 100644 index 0000000..d394ee7 --- /dev/null +++ b/models/databasehelper.go @@ -0,0 +1,75 @@ +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") + +func convertPostgresError(err error) error { + const uniqueConstraintErrorCode = "23505" + + if postgresErr, ok := err.(*pq.Error); ok { + if postgresErr.Code == uniqueConstraintErrorCode { + return UniqueConstraintError + } + } + + return err +} + +func (db *DB) execOneResult(sqlQuery string, object interface{}, args ...interface{}) error { + + rows, err := db.Query(sqlQuery, args...) + if err != nil { + return convertPostgresError(err) + } + defer rows.Close() + + foundResult := false + for rows.Next() { + + if foundResult { + return QueryResultContainedMultipleRowsError + } + + if err := rows.Scan(object); err != nil { + return convertPostgresError(err) + } + + foundResult = true + } + + if !foundResult { + return QueryResultContainedNoRowsError + } + + if err := rows.Err(); err != nil { + return convertPostgresError(err) + } + + return nil +} + +func (db *DB) execNoResults(sqlQuery string, args ...interface{}) error { + + rows, err := db.Query(sqlQuery, args...) + if err != nil { + } + defer rows.Close() + + if err := rows.Err(); err != nil { + return convertPostgresError(err) + } + + return nil +} diff --git a/models/datastore.go b/models/datastore.go index 88d7a4b..5779ee9 100644 --- a/models/datastore.go +++ b/models/datastore.go @@ -2,20 +2,8 @@ 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) @@ -41,15 +29,3 @@ type Datastore interface { 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 ae1f516..7b3b47e 100644 --- a/models/note.go +++ b/models/note.go @@ -27,31 +27,9 @@ func (db *DB) StoreNewNote( 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 err := db.execOneResult(sqlQuery, ¬eId, authorId, content, creationTime); err != nil { + return 0, 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/user.go b/models/user.go index b90198f..4d22481 100644 --- a/models/user.go +++ b/models/user.go @@ -51,14 +51,12 @@ func (db *DB) StoreNewUser( 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 := db.execNoResults(sqlQuery, displayName, emailAddress.String(), hashedPassword, creationTime); err != nil { + if err == UniqueConstraintError { + return EmailAddressAlreadyInUseError + } - if err := rows.Err(); err != nil { - return convertPostgresError(err) + return err } return nil @@ -69,25 +67,10 @@ func (db *DB) AuthenticateUserCredentials(emailAddress *EmailAddress, password s SELECT password FROM app_user WHERE email_address = $1` - rows, err := db.Query(sqlQuery, emailAddress.String()) - 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 := db.execOneResult(sqlQuery, &storedHashedPassword, emailAddress.String()); err != nil { + return err } if err := bcrypt.CompareHashAndPassword( @@ -109,25 +92,12 @@ func (db *DB) GetIdForUserWithEmailAddress(emailAddress *EmailAddress) (UserId, 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 := db.execOneResult(sqlQuery, &userId, emailAddress.String()); err != nil { + if err == QueryResultContainedNoRowsError { + return 0, CredentialsNotAuthorizedError } - - if err := rows.Scan(&userId); err != nil { - return 0, err - } - } - - if userId == 0 { - return 0, QueryResultContainedNoRowsError + return 0, err } return UserId(userId), nil From 7515d50fdaaf022a2f38f3e48541f8daaaf7bfd1 Mon Sep 17 00:00:00 2001 From: G Date: Tue, 18 Sep 2018 12:00:33 -0700 Subject: [PATCH 38/43] updated noresults to exec instead of query --- models/category.go | 2 +- models/databasehelper.go | 13 +++++++------ models/user.go | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/models/category.go b/models/category.go index 51708f9..514e36c 100644 --- a/models/category.go +++ b/models/category.go @@ -49,7 +49,7 @@ func (db *DB) StoreNewNoteCategoryRelationship( INSERT INTO note_to_category_relationship (note_id, category) VALUES ($1, $2)` - if err := db.execNoResults(sqlQuery, int64(noteId), category.String()); err != nil { + if _, err := db.execNoResults(sqlQuery, int64(noteId), category.String()); err != nil { if err == UniqueConstraintError { return NoteAlreadyContainsCategoryError } diff --git a/models/databasehelper.go b/models/databasehelper.go index d394ee7..c709357 100644 --- a/models/databasehelper.go +++ b/models/databasehelper.go @@ -60,16 +60,17 @@ func (db *DB) execOneResult(sqlQuery string, object interface{}, args ...interfa return nil } -func (db *DB) execNoResults(sqlQuery string, args ...interface{}) error { +func (db *DB) execNoResults(sqlQuery string, args ...interface{}) (int64, error) { - rows, err := db.Query(sqlQuery, args...) + res, err := db.Exec(sqlQuery, args...) if err != nil { + return 0, convertPostgresError(err) } - defer rows.Close() - if err := rows.Err(); err != nil { - return convertPostgresError(err) + numAffected, err := res.RowsAffected() + if err != nil { + return 0, convertPostgresError(err) } - return nil + return numAffected, nil } diff --git a/models/user.go b/models/user.go index 4d22481..338d979 100644 --- a/models/user.go +++ b/models/user.go @@ -51,7 +51,7 @@ func (db *DB) StoreNewUser( INSERT INTO app_user (display_name, email_address, password, creation_time) VALUES ($1, $2, $3, $4)` - if err := db.execNoResults(sqlQuery, displayName, emailAddress.String(), hashedPassword, creationTime); err != nil { + if _, err := db.execNoResults(sqlQuery, displayName, emailAddress.String(), hashedPassword, creationTime); err != nil { if err == UniqueConstraintError { return EmailAddressAlreadyInUseError } From 72f4cb7a9910e95ee99e555d447e3a2c2c700441 Mon Sep 17 00:00:00 2001 From: G Date: Tue, 18 Sep 2018 19:42:33 -0700 Subject: [PATCH 39/43] bonded and tested --- handlers/handlers.go | 33 ++++++++++++++++- integration_test.go | 68 +++++++++++++++++++++++++++++++++++ migrations/0000_createDbs.sql | 10 +++--- models/datastore.go | 2 ++ models/note.go | 51 ++++++++++++++++++++++++++ static/js/base.js | 32 +++++++++++++++++ templates/base.tmpl | 1 + 7 files changed, 191 insertions(+), 6 deletions(-) create mode 100644 static/js/base.js diff --git a/handlers/handlers.go b/handlers/handlers.go index c59a045..1e04b41 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -5,6 +5,7 @@ import ( "fmt" "html/template" "net/http" + "strconv" "strings" "time" @@ -299,8 +300,38 @@ func HandleNoteApiRequest( fmt.Fprint(responseWriter, string(noteString)) + case http.MethodDelete: + + id, err := strconv.ParseInt(request.URL.Query().Get("id"), 10, 64) + if err != nil { + http.Error(responseWriter, err.Error(), http.StatusBadRequest) + return + } + + noteId := models.NoteId(id) + + noteMap, err := env.Db.GetUsersNotes(userId) + if err != nil { + http.Error(responseWriter, err.Error(), http.StatusInternalServerError) + return + } + + if _, ok := noteMap[noteId]; !ok { + errorString := "No note with that Id written by you was found" + http.Error(responseWriter, errorString, http.StatusBadRequest) + return + } + + err = env.Db.DeleteNoteById(noteId) + if err != nil { + http.Error(responseWriter, err.Error(), http.StatusInternalServerError) + return + } + + responseWriter.WriteHeader(http.StatusOK) + default: - respondWithMethodNotAllowed(responseWriter, http.MethodGet, http.MethodPost) + respondWithMethodNotAllowed(responseWriter, http.MethodGet, http.MethodPost, http.MethodDelete) } } diff --git a/integration_test.go b/integration_test.go index 9aa0e8f..b5900c5 100644 --- a/integration_test.go +++ b/integration_test.go @@ -8,10 +8,15 @@ import ( "net/http" "net/http/cookiejar" "net/http/httptest" + "strconv" + // "net/url" + // "io" + "io/ioutil" "path/filepath" "reflect" "runtime" "testing" + "time" "github.com/atmiguel/cerealnotes/handlers" "github.com/atmiguel/cerealnotes/models" @@ -145,6 +150,59 @@ func TestAuthenticatedFlow(t *testing.T) { equals(t, http.StatusCreated, resp.StatusCode) } + // Delete note + { + mockDb.Func_GetUsersNotes = func(userId models.UserId) (models.NoteMap, error) { + return models.NoteMap(map[models.NoteId]*models.Note{ + models.NoteId(noteIdAsInt): &models.Note{ + AuthorId: models.UserId(userIdAsInt), + Content: content, + CreationTime: time.Now(), + }, + }), nil + } + + mockDb.Func_DeleteNoteById = func(noteid models.NoteId) error { + if int64(noteid) == noteIdAsInt { + return nil + } + + return errors.New("Somehow you didn't get the correct error") + } + + resp, err := sendDeleteRequest(client, server.URL+paths.NoteApi+"?id="+strconv.FormatInt(noteIdAsInt, 10)) + ok(t, err) + // printBody(resp) + + equals(t, http.StatusOK, resp.StatusCode) + } + +} + +// func sendDeleteRequest(client *http.Client, myUrl string, contentType string, body io.Reader) (resp *http.Response, err error) { +func sendDeleteRequest(client *http.Client, myUrl string) (resp *http.Response, err error) { + + req, err := http.NewRequest("DELETE", myUrl, nil) + + if err != nil { + return nil, err + } + + 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 @@ -155,6 +213,8 @@ type DiyMockDataStore struct { Func_StoreNewUser func(string, *models.EmailAddress, string) error Func_AuthenticateUserCredentials func(*models.EmailAddress, string) error Func_GetIdForUserWithEmailAddress func(*models.EmailAddress) (models.UserId, error) + Func_GetUsersNotes func(models.UserId) (models.NoteMap, error) + Func_DeleteNoteById func(models.NoteId) error } func (mock *DiyMockDataStore) StoreNewNote(note *models.Note) (models.NoteId, error) { @@ -177,6 +237,14 @@ func (mock *DiyMockDataStore) GetIdForUserWithEmailAddress(email *models.EmailAd return mock.Func_GetIdForUserWithEmailAddress(email) } +func (mock *DiyMockDataStore) GetUsersNotes(userId models.UserId) (models.NoteMap, error) { + return mock.Func_GetUsersNotes(userId) +} + +func (mock *DiyMockDataStore) DeleteNoteById(noteId models.NoteId) error { + return mock.Func_DeleteNoteById(noteId) +} + // assert fails the test if the condition is false. func assert(tb testing.TB, condition bool, msg string, v ...interface{}) { if !condition { diff --git a/migrations/0000_createDbs.sql b/migrations/0000_createDbs.sql index 9774b6c..f272b35 100644 --- a/migrations/0000_createDbs.sql +++ b/migrations/0000_createDbs.sql @@ -19,17 +19,17 @@ CREATE TABLE IF NOT EXISTS publication ( CREATE TABLE IF NOT EXISTS note ( id bigserial PRIMARY KEY, - author_id bigint references app_user(id) NOT NULL, + author_id bigint references app_user(id) ON DELETE CASCADE NOT NULL, content text NOT NULL, creation_time timestamp NOT NULL ); CREATE TABLE IF NOT EXISTS note_to_publication_relationship ( - note_id bigint PRIMARY KEY references note(id), - publication_id bigint references publication(id) NOT NULL + note_id bigint PRIMARY KEY references note(id) ON DELETE CASCADE, + publication_id bigint references publication(id) ON DELETE CASCADE NOT NULL ); CREATE TABLE IF NOT EXISTS note_to_category_relationship ( - note_id bigint PRIMARY KEY references note(id), - category category_type NOT NULL + note_id bigint PRIMARY KEY references note(id) ON DELETE CASCADE, + type category_type NOT NULL ); \ No newline at end of file diff --git a/models/datastore.go b/models/datastore.go index 5779ee9..b3b4d3b 100644 --- a/models/datastore.go +++ b/models/datastore.go @@ -24,6 +24,8 @@ type Datastore interface { StoreNewUser(string, *EmailAddress, string) error AuthenticateUserCredentials(*EmailAddress, string) error GetIdForUserWithEmailAddress(*EmailAddress) (UserId, error) + GetUsersNotes(UserId) (NoteMap, error) + DeleteNoteById(NoteId) error } type DB struct { diff --git a/models/note.go b/models/note.go index 7b3b47e..6fbcb75 100644 --- a/models/note.go +++ b/models/note.go @@ -1,6 +1,7 @@ package models import ( + "errors" "time" ) @@ -12,6 +13,8 @@ type Note struct { CreationTime time.Time `json:"creationTime"` } +var NoNoteFoundError = errors.New("No note with that information could be found") + // DB methods func (db *DB) StoreNewNote( @@ -33,3 +36,51 @@ func (db *DB) StoreNewNote( } return NoteId(noteId), nil } + +func (db *DB) GetUsersNotes(userId UserId) (NoteMap, error) { + noteMap := make(map[NoteId]*Note) + + { + sqlQuery := ` + SELECT id, author_id, content, creation_time FROM note + WHERE author_id = $1` + rows, err := db.Query(sqlQuery, int64(userId)) + if err != nil { + return nil, convertPostgresError(err) + } + defer rows.Close() + + for rows.Next() { + var tempId int64 + tempNote := &Note{} + if err := rows.Scan(&tempId, &tempNote.AuthorId, &tempNote.Content, &tempNote.CreationTime); err != nil { + return nil, convertPostgresError(err) + } + + noteMap[NoteId(tempId)] = tempNote + } + } + + return noteMap, nil +} + +func (db *DB) DeleteNoteById(noteId NoteId) error { + sqlQuery := ` + DELETE FROM note + WHERE id = $1` + + num, err := db.execNoResults(sqlQuery, int64(noteId)) + if err != nil { + return err + } + + if num == 0 { + return NoNoteFoundError + } + + if num != 1 { + return errors.New("Somewhere we more than 1 note was deleted") + } + + return nil +} diff --git a/static/js/base.js b/static/js/base.js new file mode 100644 index 0000000..2143555 --- /dev/null +++ b/static/js/base.js @@ -0,0 +1,32 @@ +$(function() { + // http://stepansuvorov.com/blog/2014/04/jquery-put-and-delete/ + jQuery.each( [ "put", "delete" ], function( i, method ) { + jQuery[ method ] = function( url, data, callback, type ) { + if ( jQuery.isFunction( data ) ) { + type = type || callback; + callback = data; + data = undefined; + } + + return jQuery.ajax({ + url: url, + type: method, + dataType: type, + data: data, + success: callback + }); + }; + }); + + jQuery.prototype.getDOM = function() { + if (this.length === 1) { + return this[0]; + } + + if (this.length === 0) { + throw "jQuery object is empty" + } + throw "jQuery Object contains more than 1 object"; + }; + +}); \ No newline at end of file diff --git a/templates/base.tmpl b/templates/base.tmpl index 02de033..6a2d76c 100644 --- a/templates/base.tmpl +++ b/templates/base.tmpl @@ -20,6 +20,7 @@ + {{ block "js" . }} {{ end }} From ed5679bd62a21beb0bca8ce4e2502802de8831ac Mon Sep 17 00:00:00 2001 From: G Date: Tue, 18 Sep 2018 20:15:19 -0700 Subject: [PATCH 40/43] get users works --- handlers/handlers.go | 37 +++++++++++++++++++------------------ models/datastore.go | 3 +++ models/note.go | 8 ++++++++ models/user.go | 43 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 73 insertions(+), 18 deletions(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index 1e04b41..82ea4f5 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -115,15 +115,13 @@ func HandleUserApiRequest( return } - user1 := models.User{"Adrian"} - user2 := models.User{"Evan"} - - usersById := map[models.UserId]models.User{ - 1: user1, - 2: user2, + usersById, err := env.Db.GetAllUsersById() + if err != nil { + http.Error(responseWriter, err.Error(), http.StatusInternalServerError) + return } - usersByIdJson, err := json.Marshal(usersById) + usersByIdJson, err := usersById.ToJson() if err != nil { http.Error(responseWriter, err.Error(), http.StatusInternalServerError) return @@ -231,21 +229,24 @@ func HandleNoteApiRequest( switch request.Method { case http.MethodGet: - var notesById models.NoteMap = make(map[models.NoteId]*models.Note, 2) - - notesById[models.NoteId(1)] = &models.Note{ - AuthorId: 1, - Content: "This is an example note.", - CreationTime: time.Now().Add(-oneWeek).UTC(), + publishedNotes, err := env.Db.GetAllPublishedNotesVisibleBy(userId) + if err != nil { + http.Error(responseWriter, err.Error(), http.StatusInternalServerError) + return } - notesById[models.NoteId(2)] = &models.Note{ - AuthorId: 2, - Content: "What is this site for?", - CreationTime: time.Now().Add(-60 * 12).UTC(), + myUnpublishedNotes, err := env.Db.GetMyUnpublishedNotes(userId) + + fmt.Println("number of published notes") + fmt.Println(len(publishedNotes)) + fmt.Println("number of unpublished notes") + fmt.Println(len(myUnpublishedNotes)) + + for id, note := range myUnpublishedNotes { + publishedNotes[id] = note } - notesInJson, err := notesById.ToJson() + notesInJson, err := publishedNotes.ToJson() if err != nil { http.Error(responseWriter, err.Error(), http.StatusInternalServerError) return diff --git a/models/datastore.go b/models/datastore.go index b3b4d3b..045552c 100644 --- a/models/datastore.go +++ b/models/datastore.go @@ -26,6 +26,9 @@ type Datastore interface { GetIdForUserWithEmailAddress(*EmailAddress) (UserId, error) GetUsersNotes(UserId) (NoteMap, error) DeleteNoteById(NoteId) error + GetMyUnpublishedNotes(UserId) (NoteMap, error) + GetAllUsersById() (UserMap, error) + GetAllPublishedNotesVisibleBy(UserId) (NoteMap, error) } type DB struct { diff --git a/models/note.go b/models/note.go index 6fbcb75..48423ff 100644 --- a/models/note.go +++ b/models/note.go @@ -64,6 +64,14 @@ func (db *DB) GetUsersNotes(userId UserId) (NoteMap, error) { return noteMap, nil } +func (db *DB) GetAllPublishedNotesVisibleBy(userId UserId) (NoteMap, error) { + return nil, errors.New("Not implemented") +} + +func (db *DB) GetMyUnpublishedNotes(userId UserId) (NoteMap, error) { + return nil, errors.New("Not implimented") +} + func (db *DB) DeleteNoteById(noteId NoteId) error { sqlQuery := ` DELETE FROM note diff --git a/models/user.go b/models/user.go index 338d979..9fbc165 100644 --- a/models/user.go +++ b/models/user.go @@ -1,7 +1,9 @@ package models import ( + "encoding/json" "errors" + "fmt" "strings" "time" @@ -31,6 +33,19 @@ var EmailAddressAlreadyInUseError = errors.New("Email address already in use") var CredentialsNotAuthorizedError = errors.New("The provided credentials were not found") +type UserMap map[UserId]*User + +func (userMap UserMap) ToJson() ([]byte, error) { + // json doesn't support int indexed maps + userByIdString := make(map[string]User, len(userMap)) + + for id, user := range userMap { + userByIdString[fmt.Sprint(id)] = *user + } + + return json.Marshal(userByIdString) +} + // func (db *DB) StoreNewUser( @@ -102,3 +117,31 @@ func (db *DB) GetIdForUserWithEmailAddress(emailAddress *EmailAddress) (UserId, return UserId(userId), nil } + +func (db *DB) GetAllUsersById() (UserMap, error) { + sqlQuery := ` + SELECT id, display_name FROM app_user` + + rows, err := db.Query(sqlQuery) + if err != nil { + return nil, convertPostgresError(err) + } + + defer rows.Close() + + var userMap UserMap = make(map[UserId]*User) + + for rows.Next() { + var tempId int64 + user := &User{} + if err := rows.Scan(&tempId, &user.DisplayName); err != nil { + return nil, convertPostgresError(err) + } + + userMap[UserId(tempId)] = user + + } + + return userMap, nil + +} From 3ca7403436f46657e0da1d247997d536577ff959 Mon Sep 17 00:00:00 2001 From: G Date: Tue, 18 Sep 2018 20:29:14 -0700 Subject: [PATCH 41/43] updated test get notes --- integration_test.go | 48 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/integration_test.go b/integration_test.go index b5900c5..707c8ab 100644 --- a/integration_test.go +++ b/integration_test.go @@ -117,11 +117,40 @@ func TestAuthenticatedFlow(t *testing.T) { // Test get notes { + + mockDb.Func_GetMyUnpublishedNotes = func(userId models.UserId) (models.NoteMap, error) { + if userIdAsInt != int64(userId) { + return nil, errors.New("Invalid userId passed in") + } + + return models.NoteMap(map[models.NoteId]*models.Note{ + models.NoteId(noteIdAsInt): &models.Note{ + AuthorId: models.UserId(userIdAsInt), + Content: content, + CreationTime: time.Now(), + }, + }), nil + + } + + mockDb.Func_GetAllPublishedNotesVisibleBy = func(userId models.UserId) (models.NoteMap, error) { + if userIdAsInt != int64(userId) { + return nil, errors.New("Invalid userId passed in") + } + + return models.NoteMap(map[models.NoteId]*models.Note{ + models.NoteId(44): &models.Note{ + AuthorId: models.UserId(99), + Content: "another note", + CreationTime: time.Now(), + }, + }), nil + + } + 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 @@ -215,6 +244,9 @@ type DiyMockDataStore struct { Func_GetIdForUserWithEmailAddress func(*models.EmailAddress) (models.UserId, error) Func_GetUsersNotes func(models.UserId) (models.NoteMap, error) Func_DeleteNoteById func(models.NoteId) error + Func_GetMyUnpublishedNotes func(models.UserId) (models.NoteMap, error) + Func_GetAllUsersById func() (models.UserMap, error) + Func_GetAllPublishedNotesVisibleBy func(models.UserId) (models.NoteMap, error) } func (mock *DiyMockDataStore) StoreNewNote(note *models.Note) (models.NoteId, error) { @@ -245,6 +277,18 @@ func (mock *DiyMockDataStore) DeleteNoteById(noteId models.NoteId) error { return mock.Func_DeleteNoteById(noteId) } +func (mock *DiyMockDataStore) GetMyUnpublishedNotes(userId models.UserId) (models.NoteMap, error) { + return mock.Func_GetMyUnpublishedNotes(userId) +} + +func (mock *DiyMockDataStore) GetAllUsersById() (models.UserMap, error) { + return mock.Func_GetAllUsersById() +} + +func (mock *DiyMockDataStore) GetAllPublishedNotesVisibleBy(userId models.UserId) (models.NoteMap, error) { + return mock.Func_GetAllPublishedNotesVisibleBy(userId) +} + // assert fails the test if the condition is false. func assert(tb testing.TB, condition bool, msg string, v ...interface{}) { if !condition { From 90912095fb6bfe1178adbab2550f3365f271cfe4 Mon Sep 17 00:00:00 2001 From: G Date: Tue, 18 Sep 2018 23:02:21 -0700 Subject: [PATCH 42/43] added get notes integration test, and a bunch of database tests --- handlers/handlers.go | 12 +++- integration_test.go | 22 +++--- models/datastore.go | 2 +- models/datastore_test.go | 58 +++++++++++++++- models/note.go | 143 ++++++++++++++++++++++++++++++++------- 5 files changed, 199 insertions(+), 38 deletions(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index 82ea4f5..704e660 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -242,11 +242,17 @@ func HandleNoteApiRequest( fmt.Println("number of unpublished notes") fmt.Println(len(myUnpublishedNotes)) - for id, note := range myUnpublishedNotes { - publishedNotes[id] = note + allNotes := myUnpublishedNotes + + // TODO figure out how to surface the publication number + // for publicationNumber, noteMap := range publishedNotes { + for _, noteMap := range publishedNotes { + for id, note := range noteMap { + allNotes[id] = note + } } - notesInJson, err := publishedNotes.ToJson() + notesInJson, err := allNotes.ToJson() if err != nil { http.Error(responseWriter, err.Error(), http.StatusInternalServerError) return diff --git a/integration_test.go b/integration_test.go index 707c8ab..929c29f 100644 --- a/integration_test.go +++ b/integration_test.go @@ -133,18 +133,20 @@ func TestAuthenticatedFlow(t *testing.T) { } - mockDb.Func_GetAllPublishedNotesVisibleBy = func(userId models.UserId) (models.NoteMap, error) { + mockDb.Func_GetAllPublishedNotesVisibleBy = func(userId models.UserId) (map[int64]models.NoteMap, error) { if userIdAsInt != int64(userId) { return nil, errors.New("Invalid userId passed in") } - return models.NoteMap(map[models.NoteId]*models.Note{ - models.NoteId(44): &models.Note{ - AuthorId: models.UserId(99), - Content: "another note", - CreationTime: time.Now(), - }, - }), nil + return map[int64]models.NoteMap{ + 1: models.NoteMap(map[models.NoteId]*models.Note{ + models.NoteId(44): &models.Note{ + AuthorId: models.UserId(99), + Content: "another note", + CreationTime: time.Now(), + }, + }), + }, nil } @@ -246,7 +248,7 @@ type DiyMockDataStore struct { Func_DeleteNoteById func(models.NoteId) error Func_GetMyUnpublishedNotes func(models.UserId) (models.NoteMap, error) Func_GetAllUsersById func() (models.UserMap, error) - Func_GetAllPublishedNotesVisibleBy func(models.UserId) (models.NoteMap, error) + Func_GetAllPublishedNotesVisibleBy func(models.UserId) (map[int64]models.NoteMap, error) } func (mock *DiyMockDataStore) StoreNewNote(note *models.Note) (models.NoteId, error) { @@ -285,7 +287,7 @@ func (mock *DiyMockDataStore) GetAllUsersById() (models.UserMap, error) { return mock.Func_GetAllUsersById() } -func (mock *DiyMockDataStore) GetAllPublishedNotesVisibleBy(userId models.UserId) (models.NoteMap, error) { +func (mock *DiyMockDataStore) GetAllPublishedNotesVisibleBy(userId models.UserId) (map[int64]models.NoteMap, error) { return mock.Func_GetAllPublishedNotesVisibleBy(userId) } diff --git a/models/datastore.go b/models/datastore.go index 045552c..094fd57 100644 --- a/models/datastore.go +++ b/models/datastore.go @@ -28,7 +28,7 @@ type Datastore interface { DeleteNoteById(NoteId) error GetMyUnpublishedNotes(UserId) (NoteMap, error) GetAllUsersById() (UserMap, error) - GetAllPublishedNotesVisibleBy(UserId) (NoteMap, error) + GetAllPublishedNotesVisibleBy(UserId) (map[int64]NoteMap, error) } type DB struct { diff --git a/models/datastore_test.go b/models/datastore_test.go index ff21c92..4d1327e 100644 --- a/models/datastore_test.go +++ b/models/datastore_test.go @@ -60,11 +60,21 @@ func TestUser(t *testing.T) { err = db.StoreNewUser(displayName, emailAddress, password) ok(t, err) - _, err = db.GetIdForUserWithEmailAddress(emailAddress) + id, err := db.GetIdForUserWithEmailAddress(emailAddress) ok(t, err) err = db.AuthenticateUserCredentials(emailAddress, password) ok(t, err) + + userMap, err := db.GetAllUsersById() + ok(t, err) + + equals(t, 1, len(userMap)) + + user, isOk := userMap[id] + assert(t, isOk, "Expected UserId missing") + + equals(t, displayName, user.DisplayName) } func TestNote(t *testing.T) { @@ -87,6 +97,52 @@ func TestNote(t *testing.T) { id, err := db.StoreNewNote(note) ok(t, err) assert(t, int64(id) > 0, "Note Id was not a valid index: "+strconv.Itoa(int(id))) + + notemap, err := db.GetMyUnpublishedNotes(userId) + ok(t, err) + + retrievedNote, isOk := notemap[id] + assert(t, isOk, "Expected NoteId missing") + + equals(t, note.AuthorId, retrievedNote.AuthorId) + equals(t, note.Content, retrievedNote.Content) + + err = db.DeleteNoteById(id) + ok(t, err) +} + +func TestPublication(t *testing.T) { + db, err := models.ConnectToDatabase(postgresUrl) + ok(t, err) + ClearValuesInTable(db, userTable) + ClearValuesInTable(db, noteTable) + ClearValuesInTable(db, publicationTable) + ClearValuesInTable(db, noteToPublicationTable) + + 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))) + + fmt.Println(userId) + publicationToNoteMap, err := db.GetAllPublishedNotesVisibleBy(userId) + ok(t, err) + + equals(t, 0, len(publicationToNoteMap)) + + // TODO once we implement publication publishing, test publication adding, + // and that GetAllPublishedNotesVisibleBy has non-zero rows + } func TestCategory(t *testing.T) { diff --git a/models/note.go b/models/note.go index 48423ff..dbd1aec 100644 --- a/models/note.go +++ b/models/note.go @@ -38,38 +38,135 @@ func (db *DB) StoreNewNote( } func (db *DB) GetUsersNotes(userId UserId) (NoteMap, error) { - noteMap := make(map[NoteId]*Note) + sqlQuery := ` + SELECT id, author_id, content, creation_time FROM note + WHERE author_id = $1` - { - sqlQuery := ` - SELECT id, author_id, content, creation_time FROM note - WHERE author_id = $1` - rows, err := db.Query(sqlQuery, int64(userId)) - if err != nil { - return nil, convertPostgresError(err) - } - defer rows.Close() + noteMap, err := db.getNoteMap(sqlQuery, int64(userId)) + if err != nil { + return nil, err + } - for rows.Next() { - var tempId int64 - tempNote := &Note{} - if err := rows.Scan(&tempId, &tempNote.AuthorId, &tempNote.Content, &tempNote.CreationTime); err != nil { - return nil, convertPostgresError(err) - } + return noteMap, nil +} + +func (db *DB) GetAllPublishedNotesVisibleBy(userId UserId) (map[int64]NoteMap, error) { - noteMap[NoteId(tempId)] = tempNote + sqlQueryIssueNumber := ` + SELECT COUNT(*) AS IssueNumber FROM publication + WHERE publication.author_id = $1` + + var publictionIssueNumber int64 + if err := db.execOneResult(sqlQueryIssueNumber, &publictionIssueNumber, int64(userId)); err != nil { + return nil, err + } + + sqlQueryGetNotes := ` + SELECT + note.id, + note.author_id, + note.content, + note.creation_time, + filtered_pubs.rank AS publication_issue + FROM (SELECT *, + Rank() + OVER( + partition BY pub.author_id + ORDER BY pub.creation_time) + FROM publication AS pub) filtered_pubs + INNER JOIN note_to_publication_relationship AS note2pub + ON note2pub.publication_id = filtered_pubs.id + INNER JOIN note + ON note.id = note2pub.note_id + WHERE rank <= ($1)` + + // sqlQueryGetNotes := ` + // SELECT + // note.id, + // note.author_id, + // note.content, + // note.creation_time, + // note2cat.type AS category, + // filtered_pubs.rank AS publication_issue + // FROM (SELECT *, + // Rank() + // OVER( + // partition BY pub.author_id + // ORDER BY pub.creation_time) + // FROM publication AS pub) filtered_pubs + // INNER JOIN note_to_publication_relationship AS note2pub + // ON note2pub.publication_id = filtered_pubs.id + // INNER JOIN note + // ON note.id = note2pub.note_id + // LEFT OUTER JOIN note_to_category_relationship AS note2cat + // ON note.id = note2cat.note_id + // WHERE rank <= ($1)` + + rows, err := db.Query(sqlQueryGetNotes, publictionIssueNumber) + if err != nil { + return nil, convertPostgresError(err) + } + + defer rows.Close() + + pubToNoteMap := make(map[int64]NoteMap) + + for rows.Next() { + var publicationNumber int64 + var noteId int64 + note := &Note{} + if err := rows.Scan(¬eId, ¬e.AuthorId, ¬e.Content, ¬e.CreationTime, &publicationNumber); err != nil { + return nil, err + } + + noteMap, ok := pubToNoteMap[publicationNumber] + if !ok { + pubToNoteMap[publicationNumber] = make(map[NoteId]*Note) } + + noteMap[NoteId(noteId)] = note + } - return noteMap, nil + return pubToNoteMap, nil } -func (db *DB) GetAllPublishedNotesVisibleBy(userId UserId) (NoteMap, error) { - return nil, errors.New("Not implemented") +func (db *DB) GetMyUnpublishedNotes(userId UserId) (NoteMap, error) { + sqlQuery := ` + SELECT id, author_id, content, creation_time FROM note + LEFT OUTER JOIN note_to_publication_relationship AS note2pub + ON note.id = note2pub.note_id + WHERE note2pub.note_id is NULL AND note.author_id = $1` + + noteMap, err := db.getNoteMap(sqlQuery, int64(userId)) + if err != nil { + return nil, err + } + + return noteMap, nil } -func (db *DB) GetMyUnpublishedNotes(userId UserId) (NoteMap, error) { - return nil, errors.New("Not implimented") +func (db *DB) getNoteMap(sqlQuery string, args ...interface{}) (NoteMap, error) { + + noteMap := make(map[NoteId]*Note) + + rows, err := db.Query(sqlQuery, args...) + if err != nil { + return nil, convertPostgresError(err) + } + defer rows.Close() + + for rows.Next() { + var tempId int64 + tempNote := &Note{} + if err := rows.Scan(&tempId, &tempNote.AuthorId, &tempNote.Content, &tempNote.CreationTime); err != nil { + return nil, convertPostgresError(err) + } + + noteMap[NoteId(tempId)] = tempNote + } + + return noteMap, nil } func (db *DB) DeleteNoteById(noteId NoteId) error { @@ -87,7 +184,7 @@ func (db *DB) DeleteNoteById(noteId NoteId) error { } if num != 1 { - return errors.New("Somewhere we more than 1 note was deleted") + return errors.New("somehow more than 1 note was deleted") } return nil From 5205d0bacd181dfc9aec8150712842f3511b2aad Mon Sep 17 00:00:00 2001 From: G Date: Tue, 18 Sep 2018 23:52:27 -0700 Subject: [PATCH 43/43] publicatino api done and tested --- handlers/handlers.go | 24 +++++++++++++ integration_test.go | 23 +++++++++++- models/datastore.go | 2 ++ models/datastore_test.go | 7 ++-- models/note.go | 1 + models/publication.go | 77 +++++++++++++++++++++++++++++++++++++++- paths/paths.go | 9 ++--- routers/routers.go | 1 + 8 files changed, 135 insertions(+), 9 deletions(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index 704e660..d0d31ad 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -220,6 +220,25 @@ func HandleSessionApiRequest( } } +func HandlePublicationApiRequest( + env *Environment, + responseWriter http.ResponseWriter, + request *http.Request, + userId models.UserId, +) { + switch request.Method { + case http.MethodPost: + if err := env.Db.PublishNotes(userId); err != nil { + http.Error(responseWriter, err.Error(), http.StatusInternalServerError) + return + } + responseWriter.WriteHeader(http.StatusCreated) + + default: + respondWithMethodNotAllowed(responseWriter, http.MethodPost) + } +} + func HandleNoteApiRequest( env *Environment, responseWriter http.ResponseWriter, @@ -236,6 +255,10 @@ func HandleNoteApiRequest( } myUnpublishedNotes, err := env.Db.GetMyUnpublishedNotes(userId) + if err != nil { + http.Error(responseWriter, err.Error(), http.StatusInternalServerError) + return + } fmt.Println("number of published notes") fmt.Println(len(publishedNotes)) @@ -245,6 +268,7 @@ func HandleNoteApiRequest( allNotes := myUnpublishedNotes // TODO figure out how to surface the publication number + // for publicationNumber, noteMap := range publishedNotes { for _, noteMap := range publishedNotes { for id, note := range noteMap { diff --git a/integration_test.go b/integration_test.go index 929c29f..3d4ccf2 100644 --- a/integration_test.go +++ b/integration_test.go @@ -117,7 +117,6 @@ func TestAuthenticatedFlow(t *testing.T) { // Test get notes { - mockDb.Func_GetMyUnpublishedNotes = func(userId models.UserId) (models.NoteMap, error) { if userIdAsInt != int64(userId) { return nil, errors.New("Invalid userId passed in") @@ -181,6 +180,18 @@ func TestAuthenticatedFlow(t *testing.T) { equals(t, http.StatusCreated, resp.StatusCode) } + // Test publish notes + { + mockDb.Func_PublishNotes = func(userId models.UserId) error { + return nil + } + // publish new api + resp, err := client.Post(server.URL+paths.PublicationApi, "", nil) + printBody(resp) + ok(t, err) + equals(t, http.StatusCreated, resp.StatusCode) + } + // Delete note { mockDb.Func_GetUsersNotes = func(userId models.UserId) (models.NoteMap, error) { @@ -249,6 +260,8 @@ type DiyMockDataStore struct { Func_GetMyUnpublishedNotes func(models.UserId) (models.NoteMap, error) Func_GetAllUsersById func() (models.UserMap, error) Func_GetAllPublishedNotesVisibleBy func(models.UserId) (map[int64]models.NoteMap, error) + Func_PublishNotes func(models.UserId) error + Func_StoreNewPublication func(*models.Publication) (models.PublicationId, error) } func (mock *DiyMockDataStore) StoreNewNote(note *models.Note) (models.NoteId, error) { @@ -291,6 +304,14 @@ func (mock *DiyMockDataStore) GetAllPublishedNotesVisibleBy(userId models.UserId return mock.Func_GetAllPublishedNotesVisibleBy(userId) } +func (mock *DiyMockDataStore) PublishNotes(userId models.UserId) error { + return mock.Func_PublishNotes(userId) +} + +func (mock *DiyMockDataStore) StoreNewPublication(publication *models.Publication) (models.PublicationId, error) { + return mock.Func_StoreNewPublication(publication) +} + // assert fails the test if the condition is false. func assert(tb testing.TB, condition bool, msg string, v ...interface{}) { if !condition { diff --git a/models/datastore.go b/models/datastore.go index 094fd57..9fca1c0 100644 --- a/models/datastore.go +++ b/models/datastore.go @@ -29,6 +29,8 @@ type Datastore interface { GetMyUnpublishedNotes(UserId) (NoteMap, error) GetAllUsersById() (UserMap, error) GetAllPublishedNotesVisibleBy(UserId) (map[int64]NoteMap, error) + PublishNotes(UserId) error + StoreNewPublication(*Publication) (PublicationId, error) } type DB struct { diff --git a/models/datastore_test.go b/models/datastore_test.go index 4d1327e..6a9815a 100644 --- a/models/datastore_test.go +++ b/models/datastore_test.go @@ -134,15 +134,16 @@ func TestPublication(t *testing.T) { ok(t, err) assert(t, int64(id) > 0, "Note Id was not a valid index: "+strconv.Itoa(int(id))) - fmt.Println(userId) publicationToNoteMap, err := db.GetAllPublishedNotesVisibleBy(userId) ok(t, err) equals(t, 0, len(publicationToNoteMap)) - // TODO once we implement publication publishing, test publication adding, - // and that GetAllPublishedNotesVisibleBy has non-zero rows + err = db.PublishNotes(userId) + ok(t, err) + publicationToNoteMap, err = db.GetAllPublishedNotesVisibleBy(userId) + equals(t, 1, len(publicationToNoteMap)) } func TestCategory(t *testing.T) { diff --git a/models/note.go b/models/note.go index dbd1aec..986e268 100644 --- a/models/note.go +++ b/models/note.go @@ -122,6 +122,7 @@ func (db *DB) GetAllPublishedNotesVisibleBy(userId UserId) (map[int64]NoteMap, e noteMap, ok := pubToNoteMap[publicationNumber] if !ok { pubToNoteMap[publicationNumber] = make(map[NoteId]*Note) + noteMap = pubToNoteMap[publicationNumber] } noteMap[NoteId(noteId)] = note diff --git a/models/publication.go b/models/publication.go index 6091325..30a94b6 100644 --- a/models/publication.go +++ b/models/publication.go @@ -1,6 +1,10 @@ package models -import "time" +import ( + "errors" + "strconv" + "time" +) type PublicationId int64 @@ -8,3 +12,74 @@ type Publication struct { AuthorId UserId `json:"authorId"` CreationTime time.Time `json:"creationTime"` } + +var NoNotesToPublishError = errors.New("There are no unpublished notes to publish") + +func (db *DB) PublishNotes(userId UserId) error { + + myUnpublishedNotes, err := db.GetMyUnpublishedNotes(userId) + if err != nil { + return err + } + + if len(myUnpublishedNotes) == 0 { + return NoNotesToPublishError + } + + publicationId, err := db.StoreNewPublication(&Publication{AuthorId: userId, CreationTime: time.Now().UTC()}) + + sqlQuery := ` + INSERT INTO note_to_publication_relationship (publication_id, note_id) + VALUES ($1, $2)` + + noteIds := make([]int64, len(myUnpublishedNotes)) + + { + i := 0 + for noteId := range myUnpublishedNotes { + noteIds[i] = int64(noteId) + i++ + } + } + + values := make([]interface{}, len(noteIds)*2, len(noteIds)*2) + { + values[0] = publicationId + values[1] = noteIds[0] + for index, noteId := range noteIds { + if index == 0 { + continue + } + sqlQuery += ", ($" + strconv.Itoa(2*index+1) + ", $" + strconv.Itoa((2*index)+2) + ")" + values[2*index] = publicationId + values[(2*index)+1] = noteId + } + } + + rowAffected, err := db.execNoResults(sqlQuery, values...) + if err != nil { + return err + } + + if rowAffected < 1 { + return errors.New("No values were inserted") + } + + return nil + +} + +func (db *DB) StoreNewPublication(publication *Publication) (PublicationId, error) { + + sqlQuery := ` + INSERT INTO publication (author_id, creation_time) + VALUES ($1, $2) + RETURNING id` + + var publicationId int64 = 0 + if err := db.execOneResult(sqlQuery, &publicationId, int64(publication.AuthorId), publication.CreationTime); err != nil { + return 0, err + } + + return PublicationId(publicationId), nil +} diff --git a/paths/paths.go b/paths/paths.go index fedf71a..4d84ff2 100644 --- a/paths/paths.go +++ b/paths/paths.go @@ -8,8 +8,9 @@ const ( HomePage = "/home" NotesPage = "/notes" - UserApi = "/api/user" - SessionApi = "/api/session" - NoteApi = "/api/note" - CategoryApi = "/api/Category" + UserApi = "/api/user" + SessionApi = "/api/session" + NoteApi = "/api/note" + CategoryApi = "/api/category" + PublicationApi = "/api/publication" ) diff --git a/routers/routers.go b/routers/routers.go index 21c0dcd..96733ae 100644 --- a/routers/routers.go +++ b/routers/routers.go @@ -71,6 +71,7 @@ func DefineRoutes(env *handlers.Environment) http.Handler { mux.handleAuthenticatedApi(env, paths.NoteApi, handlers.HandleNoteApiRequest) mux.handleAuthenticatedApi(env, paths.CategoryApi, handlers.HandleCategoryApiRequest) + mux.handleAuthenticatedApi(env, paths.PublicationApi, handlers.HandlePublicationApiRequest) return mux }