diff --git a/Dockerfile b/Dockerfile index d2cdeeb..b6454eb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,8 @@ RUN go mod download && go mod verify COPY . . RUN go build -v -o log101-dot-dev-services +RUN go test -v ./... + FROM alpine WORKDIR /root/app COPY --from=builder /usr/src/app . diff --git a/db/db.go b/db/db.go index b6cadf0..f2b295c 100644 --- a/db/db.go +++ b/db/db.go @@ -20,6 +20,7 @@ func InitDB() { } db.AutoMigrate(&models.EmojiReaction{}) + db.AutoMigrate(&models.Comment{}) } func GetDB() *gorm.DB { diff --git a/handlers/comment.go b/handlers/comment.go new file mode 100644 index 0000000..d947811 --- /dev/null +++ b/handlers/comment.go @@ -0,0 +1,66 @@ +// HANDLERS FOR COMMENTING ON POSTS +package handlers + +import ( + "net/http" + + "github.com/gin-gonic/gin" + + DB "log101-blog-services/db" + "log101-blog-services/models" +) + +func GetComments(c *gin.Context) { + db := DB.GetDB() + postId := c.Query("postId") + comments := []models.Comment{} + + // Post id is required + if postId == "" { + c.AbortWithStatus(http.StatusBadRequest) + return + } + + // Retrieve comments related to post + rows := db.Where("post_id = ?", postId).Find(&comments) + if rows.Error != nil { + c.AbortWithStatus(http.StatusInternalServerError) + return + } + + // If there are no rows return empty response + if rows.RowsAffected == 0 { + c.AbortWithStatus(http.StatusNoContent) + return + } else { + c.HTML(http.StatusOK, "comments.tmpl", gin.H{"Comments": comments}) + } + +} + +func PostComment(c *gin.Context) { + db := DB.GetDB() + postId := c.PostForm("postId") + commentBody := c.PostForm("commentBody") + username := c.PostForm("username") + + // post id and comment text is necessary + if postId == "" || commentBody == "" { + c.AbortWithStatus(http.StatusBadRequest) + return + } + + // username is anonymous if not present + if username == "" { + username = "anonim" + } + + // save comment to database + result := db.Create(&models.Comment{Body: commentBody, PostId: postId, Username: username}) + if result.Error != nil { + c.AbortWithStatus(http.StatusInternalServerError) + return + } + + c.HTML(http.StatusOK, "comment.tmpl", gin.H{"Username": username, "Body": commentBody}) +} diff --git a/handlers/comment_test.go b/handlers/comment_test.go new file mode 100644 index 0000000..5ff8cff --- /dev/null +++ b/handlers/comment_test.go @@ -0,0 +1,106 @@ +package handlers + +import ( + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/gin-gonic/gin" + + "github.com/stretchr/testify/assert" + + DB "log101-blog-services/db" + "log101-blog-services/models" + "log101-blog-services/utils" +) + +func TestGetComments(t *testing.T) { + db := utils.SetupTestDB() + DB.SetDB(db) + + router := gin.Default() + router.LoadHTMLGlob("../templates/*") + + // Populate the test database with data + // Create a comment with a username + db.Create(&models.Comment{PostId: "1", Body: "sample body 1", Username: "username1"}) + db.Create(&models.Comment{PostId: "1", Body: "sample body 2", Username: "username2"}) + + router.GET("/comments", GetComments) + + req, _ := http.NewRequest("GET", "/comments?postId=1", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Contains(t, w.Body.String(), "sample body 1") + assert.Contains(t, w.Body.String(), "sample body 2") +} + +func TestPostComment(t *testing.T) { + db := utils.SetupTestDB() + DB.SetDB(db) // Set the mock DB + + router := gin.Default() + router.LoadHTMLGlob("../templates/*") + + router.POST("/comments", PostComment) + + form := url.Values{} + form.Add("postId", "1") + form.Add("commentBody", "sample comment 1") + form.Add("username", "username1") + + req, _ := http.NewRequest("POST", "/comments", strings.NewReader(form.Encode())) + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Contains(t, w.Body.String(), "sample comment 1") +} + +func TestPostAnonymousComment(t *testing.T) { + db := utils.SetupTestDB() + DB.SetDB(db) // Set the mock DB + + router := gin.Default() + router.LoadHTMLGlob("../templates/*") + + router.POST("/comments", PostComment) + + form := url.Values{} + form.Add("postId", "1") + form.Add("commentBody", "sample comment 1") + + req, _ := http.NewRequest("POST", "/comments", strings.NewReader(form.Encode())) + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Contains(t, w.Body.String(), "sample comment 1") + assert.Contains(t, w.Body.String(), "anonim") +} + +func TestPostCommentMissingParams(t *testing.T) { + db := utils.SetupTestDB() + DB.SetDB(db) // Set the mock DB + + router := gin.Default() + router.LoadHTMLGlob("../templates/*") // Ensure you have your templates in the right path + + router.POST("/comments", PostComment) + + form := url.Values{} + form.Add("postId", "1") + + req, _ := http.NewRequest("POST", "/comments", strings.NewReader(form.Encode())) + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) +} diff --git a/handlers/emoji_form.go b/handlers/emoji_form.go index 3be3824..ea2afe0 100644 --- a/handlers/emoji_form.go +++ b/handlers/emoji_form.go @@ -1,3 +1,5 @@ +// HANDLERS FOR EMOJI FORMS AND EMOJI REACTIONS +// BELOW POSTS package handlers import ( @@ -15,9 +17,9 @@ func GetEmojiForm(c *gin.Context) { postId := c.Query("postId") // get emoji counts for each emoji - emojiCounter, err := CountEmojis(postId) + emojiCounter, err := GetEmojis(postId) if err != nil { - c.HTML(http.StatusOK, "emoji_form.tmpl", gin.H{"error": "error getting the emoji counts"}) + c.AbortWithStatus(http.StatusInternalServerError) return } @@ -34,8 +36,7 @@ func PostEmojiForm(c *gin.Context) { // Check if parameters are missing if reactedPostId == "" || reaction == "" { - c.HTML(http.StatusOK, "emoji_form_error.tmpl", gin.H{"errorMessage": "missing parameters"}) - return + c.AbortWithStatus(http.StatusBadRequest) } // Add the new emoji reaction to the database @@ -43,15 +44,13 @@ func PostEmojiForm(c *gin.Context) { DoUpdates: clause.AssignmentColumns([]string{"emoji"}), }).Create(&models.EmojiReaction{UserAnonIp: c.Request.RemoteAddr, Emoji: reaction, PostId: reactedPostId}) if result.Error != nil { - c.HTML(http.StatusOK, "emoji_form_error.tmpl", gin.H{"errorMessage": "error writing to database"}) - return + c.AbortWithStatus(http.StatusInternalServerError) } // get emoji counts for each emoji - emojiCounter, err := CountEmojis(reactedPostId) + emojiCounter, err := GetEmojis(reactedPostId) if err != nil { - c.HTML(http.StatusOK, "emoji_form_error.tmpl", gin.H{"errorMessage": "error getting the emoji counts"}) - return + c.AbortWithStatus(http.StatusInternalServerError) } // Return the html with the updated emoji counter @@ -59,7 +58,7 @@ func PostEmojiForm(c *gin.Context) { } // Get the emoji counts foe a given post id -func CountEmojis(postId string) (map[string]int, error) { +func GetEmojis(postId string) (map[string]int, error) { postReactions := []models.PostReaction{} db := DB.GetDB() diff --git a/handlers/emoji_form_test.go b/handlers/emoji_form_test.go index e89bfec..e596397 100644 --- a/handlers/emoji_form_test.go +++ b/handlers/emoji_form_test.go @@ -8,30 +8,19 @@ import ( "testing" "github.com/gin-gonic/gin" - "github.com/glebarez/sqlite" "github.com/stretchr/testify/assert" - "gorm.io/gorm" DB "log101-blog-services/db" "log101-blog-services/models" + "log101-blog-services/utils" ) -func SetupTestDB() *gorm.DB { - db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) - if err != nil { - panic("failed to connect to in-memory database") - } - - db.AutoMigrate(&models.EmojiReaction{}) - return db -} - func TestGetEmojiForm(t *testing.T) { - db := SetupTestDB() + db := utils.SetupTestDB() DB.SetDB(db) router := gin.Default() - router.LoadHTMLGlob("../templates/*") // Ensure you have your templates in the right path + router.LoadHTMLGlob("../templates/*") router.GET("/emoji_form", GetEmojiForm) @@ -45,8 +34,8 @@ func TestGetEmojiForm(t *testing.T) { } func TestPostEmojiForm(t *testing.T) { - db := SetupTestDB() - DB.SetDB(db) // Set the mock DB + db := utils.SetupTestDB() + DB.SetDB(db) router := gin.Default() router.LoadHTMLGlob("../templates/*") @@ -67,8 +56,8 @@ func TestPostEmojiForm(t *testing.T) { } func TestPostEmojiFormMissingParams(t *testing.T) { - db := SetupTestDB() - DB.SetDB(db) // Set the mock DB + db := utils.SetupTestDB() + DB.SetDB(db) router := gin.Default() router.LoadHTMLGlob("../templates/*") // Ensure you have your templates in the right path @@ -83,19 +72,18 @@ func TestPostEmojiFormMissingParams(t *testing.T) { w := httptest.NewRecorder() router.ServeHTTP(w, req) - assert.Equal(t, http.StatusOK, w.Code) - assert.Contains(t, w.Body.String(), "missing parameters") + assert.Equal(t, http.StatusBadRequest, w.Code) } -func TestCountEmojis(t *testing.T) { - db := SetupTestDB() - DB.SetDB(db) // Set the mock DB +func TestGetEmojis(t *testing.T) { + db := utils.SetupTestDB() + DB.SetDB(db) // Populate the test database with data db.Create(&models.EmojiReaction{UserAnonIp: "127.0.0.2", Emoji: "😊", PostId: "1"}) db.Create(&models.EmojiReaction{UserAnonIp: "127.0.0.1", Emoji: "😂", PostId: "1"}) - counts, err := CountEmojis("1") + counts, err := GetEmojis("1") assert.NoError(t, err) assert.Equal(t, 1, counts["😊"]) assert.Equal(t, 1, counts["😂"]) diff --git a/main.go b/main.go index 03e5be1..e439396 100644 --- a/main.go +++ b/main.go @@ -42,15 +42,21 @@ func main() { r.Use(cors.New(corsConfig)) r.Use(middleware.AnonymizeIPMiddleware()) - blogForm := r.Group("/blog/api/") + blog := r.Group("/api/blog/") { - // Get the emoji form, you must provide postId query parameter - blogForm.GET("/forms/emoji", handlers.GetEmojiForm) + // Get the emoji form, postId query parameter is required + blog.GET("/forms/emoji", handlers.GetEmojiForm) // Update the user's reaction to post, this handler will // add a new entry to the database with anonymized ip // updates if user reacted before - blogForm.POST("/forms/emoji/post", handlers.PostEmojiForm) + blog.POST("/forms/emoji", handlers.PostEmojiForm) + + // Get the comments for a post, postId query parameter is required + blog.GET("/comments", handlers.GetComments) + + // Drop comment on a post, postId and comment body is required + blog.POST("/comments", handlers.PostComment) } r.Run(":8000") diff --git a/models/Comment.go b/models/Comment.go new file mode 100644 index 0000000..6f5cc50 --- /dev/null +++ b/models/Comment.go @@ -0,0 +1,8 @@ +package models + +// Gorm model +type Comment struct { + Body string + PostId string + Username string +} diff --git a/templates/comment.tmpl b/templates/comment.tmpl new file mode 100644 index 0000000..067ca4d --- /dev/null +++ b/templates/comment.tmpl @@ -0,0 +1,11 @@ +
+

+ {{ .Username }} +

+ +

+ {{ .Body }} +

+
diff --git a/templates/comments.tmpl b/templates/comments.tmpl new file mode 100644 index 0000000..ad768ad --- /dev/null +++ b/templates/comments.tmpl @@ -0,0 +1,3 @@ +{{ range .Comments }} + {{ template "comment.tmpl" . }} +{{ end }} diff --git a/templates/emoji_form.tmpl b/templates/emoji_form.tmpl index 32672e2..0d39924 100644 --- a/templates/emoji_form.tmpl +++ b/templates/emoji_form.tmpl @@ -1,9 +1,9 @@
- - - - - - + + + + + +
- +

{{ .errorMessage }}

diff --git a/templates/emoji_form_error.tmpl b/templates/emoji_form_error.tmpl deleted file mode 100644 index 3d9a8c1..0000000 --- a/templates/emoji_form_error.tmpl +++ /dev/null @@ -1,9 +0,0 @@ -
- - - - - - -
-

{{ .errorMessage }}

diff --git a/utils/main.go b/utils/main.go new file mode 100644 index 0000000..97172a7 --- /dev/null +++ b/utils/main.go @@ -0,0 +1,21 @@ +package utils + +import ( + "log101-blog-services/models" + + "github.com/glebarez/sqlite" + "gorm.io/gorm" +) + +// Init in memory sql database for testing +func SetupTestDB() *gorm.DB { + db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + if err != nil { + panic("failed to connect to in-memory database") + } + + // create tables + db.AutoMigrate(&models.EmojiReaction{}) + db.AutoMigrate(&models.Comment{}) + return db +}