package articles import ( "bytes" "fmt" "net/http" "net/http/httptest" "os" "testing" "github.com/gin-gonic/gin" "github.com/gothinkster/golang-gin-realworld-example-app/common" "github.com/gothinkster/golang-gin-realworld-example-app/users" "github.com/stretchr/testify/assert" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" ) var test_db *gorm.DB func setupRouter() *gin.Engine { r := gin.New() r.RedirectTrailingSlash = false v1 := r.Group("/api") users.UsersRegister(v1.Group("/users")) v1.Use(users.AuthMiddleware(false)) ArticlesAnonymousRegister(v1.Group("/articles")) TagsAnonymousRegister(v1.Group("/tags")) v1.Use(users.AuthMiddleware(true)) ArticlesRegister(v1.Group("/articles")) return r } func createTestUser() users.UserModel { // Generate a proper password hash to satisfy NOT NULL constraint passwordHash, _ := bcrypt.GenerateFromPassword([]byte("testpassword123"), bcrypt.DefaultCost) userModel := users.UserModel{ Username: fmt.Sprintf("testuser%d", common.RandInt()), Email: fmt.Sprintf("test%d@example.com", common.RandInt()), Bio: "test bio", PasswordHash: string(passwordHash), } test_db.Create(&userModel) return userModel } // createArticleWithUser creates a test article with an author user func createArticleWithUser(title, slug string) (ArticleModel, users.UserModel) { user := createTestUser() articleUserModel := GetArticleUserModel(user) article := ArticleModel{ Slug: slug, Title: title, Description: "Test Description", Body: "Test Body", Author: articleUserModel, AuthorID: articleUserModel.ID, } SaveOne(&article) return article, user } func TestArticleModel(t *testing.T) { asserts := assert.New(t) // Test article creation userModel := users.UserModel{ Username: "testuser", Email: "test@example.com", Bio: "test bio", } test_db.Create(&userModel) articleUserModel := GetArticleUserModel(userModel) asserts.NotEqual(uint(0), articleUserModel.ID, "ArticleUserModel should be created") asserts.Equal(userModel.ID, articleUserModel.UserModelID, "UserModelID should match") // Test article creation and save article := ArticleModel{ Slug: "test-article", Title: "Test Article", Description: "Test Description", Body: "Test Body", Author: articleUserModel, AuthorID: articleUserModel.ID, } err := SaveOne(&article) asserts.NoError(err, "Article should be saved successfully") asserts.NotEqual(uint(0), article.ID, "Article ID should be set") // Test FindOneArticle foundArticle, err := FindOneArticle(&ArticleModel{Slug: "test-article"}) asserts.NoError(err, "Article should be found") asserts.Equal("test-article", foundArticle.Slug, "Slug should match") asserts.Equal("Test Article", foundArticle.Title, "Title should match") // Test favoritesCount count := article.favoritesCount() asserts.Equal(uint(0), count, "Favorites count should be 0 initially") // Test isFavoriteBy isFav := article.isFavoriteBy(articleUserModel) asserts.False(isFav, "Article should not be favorited initially") // Test favoriteBy err = article.favoriteBy(articleUserModel) asserts.NoError(err, "Favorite should succeed") isFav = article.isFavoriteBy(articleUserModel) asserts.True(isFav, "Article should be favorited after favoriteBy") count = article.favoritesCount() asserts.Equal(uint(1), count, "Favorites count should be 1 after favoriting") // Test unFavoriteBy err = article.unFavoriteBy(articleUserModel) asserts.NoError(err, "UnFavorite should succeed") isFav = article.isFavoriteBy(articleUserModel) asserts.False(isFav, "Article should not be favorited after unFavoriteBy") count = article.favoritesCount() asserts.Equal(uint(0), count, "Favorites count should be 0 after unfavoriting") // Test article Update err = article.Update(map[string]interface{}{"Title": "Updated Title"}) asserts.NoError(err, "Update should succeed") foundArticle, _ = FindOneArticle(&ArticleModel{Slug: article.Slug}) asserts.Equal("Updated Title", foundArticle.Title, "Title should be updated") // Test DeleteArticleModel err = DeleteArticleModel(&ArticleModel{Slug: article.Slug}) asserts.NoError(err, "Delete should succeed") } func TestTagModel(t *testing.T) { asserts := assert.New(t) // Create a tag tag := TagModel{Tag: "golang"} test_db.Create(&tag) asserts.NotEqual(uint(0), tag.ID, "Tag should be created") // Test getAllTags tags, err := getAllTags() asserts.NoError(err, "getAllTags should succeed") asserts.GreaterOrEqual(len(tags), 1, "Should have at least one tag") } func TestCommentModel(t *testing.T) { asserts := assert.New(t) // Create user and article userModel := users.UserModel{ Username: "commentuser", Email: "comment@example.com", Bio: "comment bio", } test_db.Create(&userModel) articleUserModel := GetArticleUserModel(userModel) article := ArticleModel{ Slug: "comment-test-article", Title: "Comment Test Article", Description: "Test Description", Body: "Test Body", Author: articleUserModel, AuthorID: articleUserModel.ID, } SaveOne(&article) // Create a comment comment := CommentModel{ ArticleID: article.ID, AuthorID: articleUserModel.ID, Body: "Test comment", } test_db.Create(&comment) asserts.NotEqual(uint(0), comment.ID, "Comment should be created") // Test getComments err := article.getComments() asserts.NoError(err, "getComments should succeed") asserts.GreaterOrEqual(len(article.Comments), 1, "Should have at least one comment") // Test DeleteCommentModel err = DeleteCommentModel(&CommentModel{Body: "Test comment"}) asserts.NoError(err, "DeleteCommentModel should succeed") } func TestFindManyArticle(t *testing.T) { asserts := assert.New(t) // Create a user and article for testing userModel := users.UserModel{ Username: fmt.Sprintf("findmanyuser%d", common.RandInt()), Email: fmt.Sprintf("findmany%d@example.com", common.RandInt()), Bio: "test bio", } test_db.Create(&userModel) articleUserModel := GetArticleUserModel(userModel) article := ArticleModel{ Slug: fmt.Sprintf("findmany-article-%d", common.RandInt()), Title: "FindMany Test Article", Description: "Test Description", Body: "Test Body", Author: articleUserModel, AuthorID: articleUserModel.ID, } article.setTags([]string{"findmanytag"}) SaveOne(&article) // Favorite the article article.favoriteBy(articleUserModel) // Test FindManyArticle with default params articles, count, err := FindManyArticle("", "", "10", "0", "") asserts.NoError(err, "FindManyArticle should succeed") asserts.GreaterOrEqual(count, 1, "Count should be at least 1") asserts.NotNil(articles, "Articles should not be nil") // Test with invalid limit/offset _, _, err = FindManyArticle("", "", "invalid", "invalid", "") asserts.NoError(err, "FindManyArticle with invalid params should succeed") // Test filter by tag _, count, err = FindManyArticle("findmanytag", "", "10", "0", "") asserts.NoError(err, "FindManyArticle by tag should succeed") asserts.GreaterOrEqual(count, 1, "Count should be at least 1 for tag filter") // Test filter by non-existent tag _, count, err = FindManyArticle("nonexistenttag", "", "10", "0", "") asserts.NoError(err, "FindManyArticle by non-existent tag should succeed") asserts.Equal(0, count, "Count should be 0 for non-existent tag") // Test filter by author _, count, err = FindManyArticle("", userModel.Username, "10", "0", "") asserts.NoError(err, "FindManyArticle by author should succeed") asserts.GreaterOrEqual(count, 1, "Count should be at least 1 for author filter") // Test filter by non-existent author _, _, err = FindManyArticle("", "nonexistentauthor", "10", "0", "") asserts.NoError(err, "FindManyArticle by non-existent author should succeed") // Test filter by favorited _, count, err = FindManyArticle("", "", "10", "0", userModel.Username) asserts.NoError(err, "FindManyArticle by favorited should succeed") asserts.GreaterOrEqual(count, 1, "Count should be at least 1 for favorited filter") // Test filter by non-existent favorited user _, _, err = FindManyArticle("", "", "10", "0", "nonexistentuser") asserts.NoError(err, "FindManyArticle by non-existent favorited should succeed") } func TestGetArticleFeed(t *testing.T) { asserts := assert.New(t) // Create a user userModel := users.UserModel{ Username: "feeduser", Email: "feed@example.com", Bio: "feed bio", } test_db.Create(&userModel) articleUserModel := GetArticleUserModel(userModel) // Test GetArticleFeed articles, count, err := articleUserModel.GetArticleFeed("10", "0") asserts.NoError(err, "GetArticleFeed should succeed") asserts.GreaterOrEqual(count, 0, "Count should be non-negative") asserts.NotNil(articles, "Articles should not be nil") } func TestSetTags(t *testing.T) { asserts := assert.New(t) // Create user and article userModel := users.UserModel{ Username: "taguser", Email: "tag@example.com", Bio: "tag bio", } test_db.Create(&userModel) articleUserModel := GetArticleUserModel(userModel) article := ArticleModel{ Slug: "tag-test-article", Title: "Tag Test Article", Description: "Test Description", Body: "Test Body", Author: articleUserModel, AuthorID: articleUserModel.ID, } // Test setTags err := article.setTags([]string{"go", "programming", "web"}) asserts.NoError(err, "setTags should succeed") asserts.Equal(3, len(article.Tags), "Should have 3 tags") } // Helper functions for router tests - used by TestArticleRouters func userModelMocker(n int) []users.UserModel { var offset int64 test_db.Model(&users.UserModel{}).Count(&offset) var ret []users.UserModel for i := int(offset) + 1; i <= int(offset)+n; i++ { image := fmt.Sprintf("http://image/%v.jpg", i) // Generate password hash directly using bcrypt passwordHash, err := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost) if err != nil { panic(fmt.Sprintf("failed to generate password hash: %v", err)) } userModel := users.UserModel{ Username: fmt.Sprintf("articleuser%v", i), Email: fmt.Sprintf("articleuser%v@test.com", i), Bio: fmt.Sprintf("bio%v", i), Image: &image, PasswordHash: string(passwordHash), } test_db.Create(&userModel) ret = append(ret, userModel) } return ret } func resetDBWithMock() { common.TestDBFree(test_db) test_db = common.TestDBInit() users.AutoMigrate() test_db.AutoMigrate(&ArticleModel{}) test_db.AutoMigrate(&TagModel{}) test_db.AutoMigrate(&FavoriteModel{}) test_db.AutoMigrate(&ArticleUserModel{}) test_db.AutoMigrate(&CommentModel{}) userModelMocker(3) } // Router tests var articleRequestTests = []struct { init func(*http.Request) url string method string bodyData string expectedCode int responseRegexp string msg string }{ // Test article list { func(req *http.Request) { resetDBWithMock() }, "/api/articles/", "GET", ``, http.StatusOK, `{"articles":\[\],"articlesCount":0}`, "empty article list should return empty array", }, // Test tags list { func(req *http.Request) {}, "/api/tags/", "GET", ``, http.StatusOK, `{"tags":\[\]}`, "empty tags list should return empty array", }, // Test create article { func(req *http.Request) { common.HeaderTokenMock(req, 1) }, "/api/articles/", "POST", `{"article":{"title":"Test Article","description":"Test Description","body":"Test Body","tagList":["test","golang"]}}`, http.StatusCreated, `"title":"Test Article"`, "create article should succeed with auth", }, // Test get single article { func(req *http.Request) {}, "/api/articles/test-article", "GET", ``, http.StatusOK, `"slug":"test-article"`, "get single article should succeed", }, // Test article list with articles { func(req *http.Request) {}, "/api/articles/", "GET", ``, http.StatusOK, `"articlesCount":1`, "article list should contain created article", }, // Test articles by tag { func(req *http.Request) {}, "/api/articles/?tag=golang", "GET", ``, http.StatusOK, `"articles":\[`, "articles by tag should work", }, // Test articles by author { func(req *http.Request) {}, "/api/articles/?author=articleuser1", "GET", ``, http.StatusOK, `"articles":\[`, "articles by author should work", }, // Test update article { func(req *http.Request) { common.HeaderTokenMock(req, 1) }, "/api/articles/test-article", "PUT", `{"article":{"title":"Updated Title"}}`, http.StatusOK, `"title":"Updated Title"`, "update article should succeed", }, // Test favorite article { func(req *http.Request) { common.HeaderTokenMock(req, 1) }, "/api/articles/updated-title/favorite", "POST", ``, http.StatusOK, `"favorited":true`, "favorite article should succeed", }, // Test favorites count { func(req *http.Request) {}, "/api/articles/updated-title", "GET", ``, http.StatusOK, `"favoritesCount":1`, "favorites count should be 1", }, // Test articles favorited by user { func(req *http.Request) {}, "/api/articles/?favorited=articleuser1", "GET", ``, http.StatusOK, `"articlesCount":1`, "articles favorited by user should work", }, // Test unfavorite article { func(req *http.Request) { common.HeaderTokenMock(req, 1) }, "/api/articles/updated-title/favorite", "DELETE", ``, http.StatusOK, `"favorited":false`, "unfavorite article should succeed", }, // Test favorites count after unfavorite { func(req *http.Request) {}, "/api/articles/updated-title", "GET", ``, http.StatusOK, `"favoritesCount":0`, "favorites count should be 0 after unfavorite", }, // Test create comment { func(req *http.Request) { common.HeaderTokenMock(req, 1) }, "/api/articles/updated-title/comments", "POST", `{"comment":{"body":"Test comment body"}}`, http.StatusCreated, `"body":"Test comment body"`, "create comment should succeed", }, // Test get comments { func(req *http.Request) {}, "/api/articles/updated-title/comments", "GET", ``, http.StatusOK, `"comments":\[`, "get comments should succeed", }, // Test delete comment { func(req *http.Request) { common.HeaderTokenMock(req, 1) }, "/api/articles/updated-title/comments/1", "DELETE", ``, http.StatusOK, ``, "delete comment should succeed", }, // Test feed (requires auth) - returns empty array since no follow relationship set up { func(req *http.Request) { common.HeaderTokenMock(req, 2) }, "/api/articles/feed", "GET", ``, http.StatusOK, `"articles":\[\]`, "feed should return empty array when user follows no one", }, // Test delete article { func(req *http.Request) { common.HeaderTokenMock(req, 1) }, "/api/articles/updated-title", "DELETE", ``, http.StatusOK, ``, "delete article should succeed", }, // Test 404 for deleted article { func(req *http.Request) {}, "/api/articles/updated-title", "GET", ``, http.StatusNotFound, `"articles":"Invalid slug"`, "deleted article should return 404", }, // Test favorite non-existent article { func(req *http.Request) { common.HeaderTokenMock(req, 1) }, "/api/articles/non-existent/favorite", "POST", ``, http.StatusNotFound, `"articles":"Invalid slug"`, "favorite non-existent article should return 404", }, // Test unfavorite non-existent article { func(req *http.Request) { common.HeaderTokenMock(req, 1) }, "/api/articles/non-existent/favorite", "DELETE", ``, http.StatusNotFound, `"articles":"Invalid slug"`, "unfavorite non-existent article should return 404", }, // Test create article with invalid data { func(req *http.Request) { common.HeaderTokenMock(req, 1) }, "/api/articles/", "POST", `{"article":{"title":"ab","description":"Test","body":"Test"}}`, http.StatusUnprocessableEntity, `"errors"`, "create article with short title should fail", }, // Test create comment on non-existent article { func(req *http.Request) { common.HeaderTokenMock(req, 1) }, "/api/articles/non-existent/comments", "POST", `{"comment":{"body":"Test"}}`, http.StatusNotFound, `"comment":"Invalid slug"`, "create comment on non-existent article should return 404", }, // Test get comments on non-existent article { func(req *http.Request) {}, "/api/articles/non-existent/comments", "GET", ``, http.StatusNotFound, `"comments":"Invalid slug"`, "get comments on non-existent article should return 404", }, // Test update non-existent article { func(req *http.Request) { common.HeaderTokenMock(req, 1) }, "/api/articles/non-existent", "PUT", `{"article":{"title":"Test"}}`, http.StatusNotFound, `"articles":"Invalid slug"`, "update non-existent article should return 404", }, // Test delete non-existent article (GORM delete returns OK even if not found) { func(req *http.Request) { common.HeaderTokenMock(req, 1) }, "/api/articles/non-existent", "DELETE", ``, http.StatusOK, ``, "delete non-existent article returns OK (soft delete behavior)", }, // Test delete comment with invalid id { func(req *http.Request) { common.HeaderTokenMock(req, 1) }, "/api/articles/test/comments/invalid", "DELETE", ``, http.StatusNotFound, `"comment":"Invalid id"`, "delete comment with invalid id should return 404", }, } func TestArticleRouters(t *testing.T) { asserts := assert.New(t) r := gin.New() r.Use(users.AuthMiddleware(false)) ArticlesAnonymousRegister(r.Group("/api/articles")) TagsAnonymousRegister(r.Group("/api/tags")) r.Use(users.AuthMiddleware(true)) ArticlesRegister(r.Group("/api/articles")) for _, testData := range articleRequestTests { bodyData := testData.bodyData req, err := http.NewRequest(testData.method, testData.url, bytes.NewBufferString(bodyData)) req.Header.Set("Content-Type", "application/json") asserts.NoError(err) testData.init(req) w := httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(testData.expectedCode, w.Code, "Response Status - "+testData.msg) if testData.responseRegexp != "" { asserts.Regexp(testData.responseRegexp, w.Body.String(), "Response Content - "+testData.msg) } } } // HTTP API Tests func TestCreateArticleRequiredFields(t *testing.T) { asserts := assert.New(t) r := setupRouter() user := createTestUser() // Test missing body field req, _ := http.NewRequest("POST", "/api/articles", bytes.NewBufferString(`{"article":{"title":"Test Title","description":"Test Description"}}`)) req.Header.Set("Content-Type", "application/json") common.HeaderTokenMock(req, user.ID) w := httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusUnprocessableEntity, w.Code, "Missing body should return 422") asserts.Contains(w.Body.String(), "Body", "Error should mention Body field") // Test missing description field req, _ = http.NewRequest("POST", "/api/articles", bytes.NewBufferString(`{"article":{"title":"Test Title","body":"Test Body"}}`)) req.Header.Set("Content-Type", "application/json") common.HeaderTokenMock(req, user.ID) w = httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusUnprocessableEntity, w.Code, "Missing description should return 422") asserts.Contains(w.Body.String(), "Description", "Error should mention Description field") // Test valid article creation req, _ = http.NewRequest("POST", "/api/articles", bytes.NewBufferString(`{"article":{"title":"Test Title","description":"Test Description","body":"Test Body"}}`)) req.Header.Set("Content-Type", "application/json") common.HeaderTokenMock(req, user.ID) w = httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusCreated, w.Code, "Valid article should return 201") asserts.Contains(w.Body.String(), `"article"`, "Response should contain article") } func TestCreateCommentRequiredFields(t *testing.T) { asserts := assert.New(t) r := setupRouter() user := createTestUser() // Create an article first articleUserModel := GetArticleUserModel(user) article := ArticleModel{ Slug: fmt.Sprintf("test-comment-article-%d", common.RandInt()), Title: "Test Comment Article", Description: "Test Description", Body: "Test Body", Author: articleUserModel, AuthorID: articleUserModel.ID, } SaveOne(&article) // Test missing body field req, _ := http.NewRequest("POST", fmt.Sprintf("/api/articles/%s/comments", article.Slug), bytes.NewBufferString(`{"comment":{}}`)) req.Header.Set("Content-Type", "application/json") common.HeaderTokenMock(req, user.ID) w := httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusUnprocessableEntity, w.Code, "Missing body should return 422") asserts.Contains(w.Body.String(), "Body", "Error should mention Body field") // Test valid comment creation - should return 201 per OpenAPI spec req, _ = http.NewRequest("POST", fmt.Sprintf("/api/articles/%s/comments", article.Slug), bytes.NewBufferString(`{"comment":{"body":"Test comment body"}}`)) req.Header.Set("Content-Type", "application/json") common.HeaderTokenMock(req, user.ID) w = httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusCreated, w.Code, "Valid comment should return 201") asserts.Contains(w.Body.String(), `"comment"`, "Response should contain comment") } func TestArticleFeedCount(t *testing.T) { asserts := assert.New(t) // Create two users user1 := createTestUser() user2 := createTestUser() // User1 follows User2 err := followUser(user1, user2) asserts.NoError(err, "Follow should succeed") // Create an article by User2 articleUserModel := GetArticleUserModel(user2) article := ArticleModel{ Slug: fmt.Sprintf("feed-test-article-%d", common.RandInt()), Title: "Feed Test Article", Description: "Test Description", Body: "Test Body", Author: articleUserModel, AuthorID: articleUserModel.ID, } SaveOne(&article) // Get feed for User1 articleUserModel1 := GetArticleUserModel(user1) articles, count, err := articleUserModel1.GetArticleFeed("10", "0") asserts.NoError(err, "GetArticleFeed should succeed") asserts.Equal(1, count, "Count should be 1 after following user with 1 article") asserts.Equal(1, len(articles), "Should have 1 article in feed") } func followUser(follower, following users.UserModel) error { db := common.GetDB() var follow users.FollowModel err := db.FirstOrCreate(&follow, &users.FollowModel{ FollowingID: following.ID, FollowedByID: follower.ID, }).Error return err } func TestTagsEndpoint(t *testing.T) { asserts := assert.New(t) r := setupRouter() // Create some tags tag1 := TagModel{Tag: fmt.Sprintf("testtag1-%d", common.RandInt())} tag2 := TagModel{Tag: fmt.Sprintf("testtag2-%d", common.RandInt())} test_db.Create(&tag1) test_db.Create(&tag2) req, _ := http.NewRequest("GET", "/api/tags", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusOK, w.Code, "Tags endpoint should return 200") asserts.Contains(w.Body.String(), `"tags"`, "Response should contain tags array") } func TestArticleListEndpoint(t *testing.T) { asserts := assert.New(t) r := setupRouter() _, _ = createArticleWithUser("List Test Article", fmt.Sprintf("list-test-article-%d", common.RandInt())) // Test list articles req, _ := http.NewRequest("GET", "/api/articles", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusOK, w.Code, "Articles list should return 200") asserts.Contains(w.Body.String(), `"articles"`, "Response should contain articles") asserts.Contains(w.Body.String(), `"articlesCount"`, "Response should contain articlesCount") } func TestArticleRetrieveEndpoint(t *testing.T) { asserts := assert.New(t) r := setupRouter() slug := fmt.Sprintf("retrieve-test-article-%d", common.RandInt()) article, _ := createArticleWithUser("Retrieve Test Article", slug) // Test retrieve article req, _ := http.NewRequest("GET", fmt.Sprintf("/api/articles/%s", article.Slug), nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusOK, w.Code, "Article retrieve should return 200") asserts.Contains(w.Body.String(), `"article"`, "Response should contain article") asserts.Contains(w.Body.String(), `"Retrieve Test Article"`, "Response should contain the article title") } func TestArticleUpdateEndpoint(t *testing.T) { asserts := assert.New(t) r := setupRouter() slug := fmt.Sprintf("update-test-article-%d", common.RandInt()) article, user := createArticleWithUser("Update Test Article", slug) // Test update article req, _ := http.NewRequest("PUT", fmt.Sprintf("/api/articles/%s", article.Slug), bytes.NewBufferString(`{"article":{"body":"Updated Body"}}`)) req.Header.Set("Content-Type", "application/json") common.HeaderTokenMock(req, user.ID) w := httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusOK, w.Code, "Article update should return 200") asserts.Contains(w.Body.String(), `"article"`, "Response should contain article") } func TestArticleDeleteEndpoint(t *testing.T) { asserts := assert.New(t) r := setupRouter() user := createTestUser() // Create an article articleUserModel := GetArticleUserModel(user) slug := fmt.Sprintf("delete-test-article-%d", common.RandInt()) article := ArticleModel{ Slug: slug, Title: "Delete Test Article", Description: "Test Description", Body: "Test Body", Author: articleUserModel, AuthorID: articleUserModel.ID, } SaveOne(&article) // Test delete article req, _ := http.NewRequest("DELETE", fmt.Sprintf("/api/articles/%s", slug), nil) common.HeaderTokenMock(req, user.ID) w := httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusOK, w.Code, "Article delete should return 200") } func TestArticleFavoriteEndpoint(t *testing.T) { asserts := assert.New(t) r := setupRouter() user := createTestUser() // Create an article articleUserModel := GetArticleUserModel(user) slug := fmt.Sprintf("favorite-test-article-%d", common.RandInt()) article := ArticleModel{ Slug: slug, Title: "Favorite Test Article", Description: "Test Description", Body: "Test Body", Author: articleUserModel, AuthorID: articleUserModel.ID, } SaveOne(&article) // Test favorite article req, _ := http.NewRequest("POST", fmt.Sprintf("/api/articles/%s/favorite", slug), nil) common.HeaderTokenMock(req, user.ID) w := httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusOK, w.Code, "Article favorite should return 200") asserts.Contains(w.Body.String(), `"favorited":true`, "Article should be favorited") // Test unfavorite article req, _ = http.NewRequest("DELETE", fmt.Sprintf("/api/articles/%s/favorite", slug), nil) common.HeaderTokenMock(req, user.ID) w = httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusOK, w.Code, "Article unfavorite should return 200") asserts.Contains(w.Body.String(), `"favorited":false`, "Article should be unfavorited") } func TestArticleCommentsEndpoint(t *testing.T) { asserts := assert.New(t) r := setupRouter() user := createTestUser() // Create an article articleUserModel := GetArticleUserModel(user) slug := fmt.Sprintf("comments-test-article-%d", common.RandInt()) article := ArticleModel{ Slug: slug, Title: "Comments Test Article", Description: "Test Description", Body: "Test Body", Author: articleUserModel, AuthorID: articleUserModel.ID, } SaveOne(&article) // Create a comment comment := CommentModel{ ArticleID: article.ID, AuthorID: articleUserModel.ID, Body: "Test comment for list", } test_db.Create(&comment) // Test list comments req, _ := http.NewRequest("GET", fmt.Sprintf("/api/articles/%s/comments", slug), nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusOK, w.Code, "Comments list should return 200") asserts.Contains(w.Body.String(), `"comments"`, "Response should contain comments") // Test delete comment req, _ = http.NewRequest("DELETE", fmt.Sprintf("/api/articles/%s/comments/%d", slug, comment.ID), nil) common.HeaderTokenMock(req, user.ID) w = httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusOK, w.Code, "Comment delete should return 200") } func TestArticleFeedEndpoint(t *testing.T) { asserts := assert.New(t) r := setupRouter() user := createTestUser() // Test feed endpoint req, _ := http.NewRequest("GET", "/api/articles/feed", nil) common.HeaderTokenMock(req, user.ID) w := httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusOK, w.Code, "Feed should return 200") asserts.Contains(w.Body.String(), `"articles"`, "Response should contain articles") asserts.Contains(w.Body.String(), `"articlesCount"`, "Response should contain articlesCount") // Test feed with limit and offset params req, _ = http.NewRequest("GET", "/api/articles/feed?limit=5&offset=0", nil) common.HeaderTokenMock(req, user.ID) w = httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusOK, w.Code, "Feed with params should return 200") } func TestArticleFeedWithFollowing(t *testing.T) { asserts := assert.New(t) r := setupRouter() // Create two users user1 := createTestUser() user2 := createTestUser() // User1 follows User2 followUser(user1, user2) // Create an article by User2 articleUserModel := GetArticleUserModel(user2) article := ArticleModel{ Slug: fmt.Sprintf("feed-following-article-%d", common.RandInt()), Title: "Feed Following Test", Description: "Test Description", Body: "Test Body", Author: articleUserModel, AuthorID: articleUserModel.ID, } SaveOne(&article) // Test feed for User1 - should include User2's article req, _ := http.NewRequest("GET", "/api/articles/feed", nil) common.HeaderTokenMock(req, user1.ID) w := httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusOK, w.Code, "Feed should return 200") asserts.Contains(w.Body.String(), `"articles"`, "Response should contain articles") } func TestArticleNotFoundErrors(t *testing.T) { asserts := assert.New(t) r := setupRouter() user := createTestUser() // Test retrieve non-existent article req, _ := http.NewRequest("GET", "/api/articles/non-existent-slug", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusNotFound, w.Code, "Non-existent article should return 404") // Test update non-existent article req, _ = http.NewRequest("PUT", "/api/articles/non-existent-slug", bytes.NewBufferString(`{"article":{"body":"test"}}`)) req.Header.Set("Content-Type", "application/json") common.HeaderTokenMock(req, user.ID) w = httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusNotFound, w.Code, "Update non-existent article should return 404") // Test delete non-existent article - returns 200 (GORM delete doesn't error on 0 rows) req, _ = http.NewRequest("DELETE", "/api/articles/non-existent-slug", nil) common.HeaderTokenMock(req, user.ID) w = httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusOK, w.Code, "Delete non-existent article returns 200") // Test favorite non-existent article req, _ = http.NewRequest("POST", "/api/articles/non-existent-slug/favorite", nil) common.HeaderTokenMock(req, user.ID) w = httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusNotFound, w.Code, "Favorite non-existent article should return 404") // Test unfavorite non-existent article req, _ = http.NewRequest("DELETE", "/api/articles/non-existent-slug/favorite", nil) common.HeaderTokenMock(req, user.ID) w = httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusNotFound, w.Code, "Unfavorite non-existent article should return 404") // Test create comment on non-existent article req, _ = http.NewRequest("POST", "/api/articles/non-existent-slug/comments", bytes.NewBufferString(`{"comment":{"body":"test"}}`)) req.Header.Set("Content-Type", "application/json") common.HeaderTokenMock(req, user.ID) w = httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusNotFound, w.Code, "Comment on non-existent article should return 404") // Test list comments on non-existent article req, _ = http.NewRequest("GET", "/api/articles/non-existent-slug/comments", nil) w = httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusNotFound, w.Code, "List comments on non-existent article should return 404") // Test delete comment with invalid id req, _ = http.NewRequest("DELETE", "/api/articles/some-slug/comments/invalid", nil) common.HeaderTokenMock(req, user.ID) w = httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusNotFound, w.Code, "Delete comment with invalid id should return 404") } func TestArticleListWithFilters(t *testing.T) { asserts := assert.New(t) r := setupRouter() // Test list with tag filter req, _ := http.NewRequest("GET", "/api/articles?tag=sometag", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusOK, w.Code, "List with tag filter should return 200") // Test list with author filter req, _ = http.NewRequest("GET", "/api/articles?author=someauthor", nil) w = httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusOK, w.Code, "List with author filter should return 200") // Test list with favorited filter req, _ = http.NewRequest("GET", "/api/articles?favorited=someuser", nil) w = httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusOK, w.Code, "List with favorited filter should return 200") // Test list with limit and offset req, _ = http.NewRequest("GET", "/api/articles?limit=5&offset=0", nil) w = httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusOK, w.Code, "List with limit/offset should return 200") } func TestArticleValidationErrors(t *testing.T) { asserts := assert.New(t) r := setupRouter() user := createTestUser() // Test create article with missing title req, _ := http.NewRequest("POST", "/api/articles", bytes.NewBufferString(`{"article":{"description":"test","body":"test"}}`)) req.Header.Set("Content-Type", "application/json") common.HeaderTokenMock(req, user.ID) w := httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusUnprocessableEntity, w.Code, "Missing title should return 422") // Test create article with short title req, _ = http.NewRequest("POST", "/api/articles", bytes.NewBufferString(`{"article":{"title":"abc","description":"test","body":"test"}}`)) req.Header.Set("Content-Type", "application/json") common.HeaderTokenMock(req, user.ID) w = httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusUnprocessableEntity, w.Code, "Short title should return 422") } func TestArticleFeedUnauthorized(t *testing.T) { asserts := assert.New(t) r := setupRouter() // Test feed endpoint without auth - should return 401 req, _ := http.NewRequest("GET", "/api/articles/feed", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusUnauthorized, w.Code, "Feed without auth should return 401") } func TestArticleUpdateValidationErrors(t *testing.T) { asserts := assert.New(t) r := setupRouter() user := createTestUser() // Create an article articleUserModel := GetArticleUserModel(user) slug := fmt.Sprintf("update-validation-article-%d", common.RandInt()) article := ArticleModel{ Slug: slug, Title: "Update Validation Test", Description: "Test Description", Body: "Test Body", Author: articleUserModel, AuthorID: articleUserModel.ID, } SaveOne(&article) // Test update with title too short req, _ := http.NewRequest("PUT", fmt.Sprintf("/api/articles/%s", slug), bytes.NewBufferString(`{"article":{"title":"ab"}}`)) req.Header.Set("Content-Type", "application/json") common.HeaderTokenMock(req, user.ID) w := httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusUnprocessableEntity, w.Code, "Short title in update should return 422") } func TestArticleCreateWithTags(t *testing.T) { asserts := assert.New(t) r := setupRouter() user := createTestUser() // Test create article with tags req, _ := http.NewRequest("POST", "/api/articles", bytes.NewBufferString(`{"article":{"title":"Test With Tags","description":"Test Description","body":"Test Body","tagList":["go","gin","test"]}}`)) req.Header.Set("Content-Type", "application/json") common.HeaderTokenMock(req, user.ID) w := httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusCreated, w.Code, "Article with tags should return 201") asserts.Contains(w.Body.String(), `"tagList"`, "Response should contain tagList") } func TestCommentDeleteWithValidArticle(t *testing.T) { asserts := assert.New(t) r := setupRouter() user := createTestUser() // Create an article articleUserModel := GetArticleUserModel(user) slug := fmt.Sprintf("comment-delete-article-%d", common.RandInt()) article := ArticleModel{ Slug: slug, Title: "Comment Delete Test", Description: "Test Description", Body: "Test Body", Author: articleUserModel, AuthorID: articleUserModel.ID, } SaveOne(&article) // Create a comment comment := CommentModel{ ArticleID: article.ID, AuthorID: articleUserModel.ID, Body: "Test comment for deletion", } test_db.Create(&comment) // Test delete existing comment req, _ := http.NewRequest("DELETE", fmt.Sprintf("/api/articles/%s/comments/%d", slug, comment.ID), nil) common.HeaderTokenMock(req, user.ID) w := httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusOK, w.Code, "Delete existing comment should return 200") } func TestSetTagsEmpty(t *testing.T) { asserts := assert.New(t) userModel := users.UserModel{ Username: fmt.Sprintf("emptytaguser%d", common.RandInt()), Email: fmt.Sprintf("emptytag%d@example.com", common.RandInt()), Bio: "test bio", } test_db.Create(&userModel) articleUserModel := GetArticleUserModel(userModel) article := ArticleModel{ Slug: fmt.Sprintf("empty-tags-article-%d", common.RandInt()), Title: "Empty Tags Test", Description: "Test Description", Body: "Test Body", Author: articleUserModel, AuthorID: articleUserModel.ID, } // Test setTags with empty slice err := article.setTags([]string{}) asserts.NoError(err, "setTags with empty slice should succeed") asserts.Equal(0, len(article.Tags), "Should have 0 tags") } func TestFavoritesCountWithMultipleUsers(t *testing.T) { asserts := assert.New(t) // Create article user1 := createTestUser() user2 := createTestUser() articleUserModel1 := GetArticleUserModel(user1) articleUserModel2 := GetArticleUserModel(user2) article := ArticleModel{ Slug: fmt.Sprintf("multi-favorite-article-%d", common.RandInt()), Title: "Multi Favorite Test", Description: "Test Description", Body: "Test Body", Author: articleUserModel1, AuthorID: articleUserModel1.ID, } SaveOne(&article) // Both users favorite the article article.favoriteBy(articleUserModel1) article.favoriteBy(articleUserModel2) count := article.favoritesCount() asserts.Equal(uint(2), count, "Favorites count should be 2") } func TestBatchGetFavoriteStatusEdgeCases(t *testing.T) { asserts := assert.New(t) user := createTestUser() articleUserModel := GetArticleUserModel(user) // Test with empty article IDs statusMap := BatchGetFavoriteStatus([]uint{}, articleUserModel.ID) asserts.Equal(0, len(statusMap), "Empty article IDs should return empty map") // Test with zero user ID article := ArticleModel{ Slug: fmt.Sprintf("batch-status-article-%d", common.RandInt()), Title: "Batch Status Test", Description: "Test Description", Body: "Test Body", Author: articleUserModel, AuthorID: articleUserModel.ID, } SaveOne(&article) article.favoriteBy(articleUserModel) statusMap = BatchGetFavoriteStatus([]uint{article.ID}, 0) asserts.Equal(0, len(statusMap), "Zero user ID should return empty map") // Test with valid IDs statusMap = BatchGetFavoriteStatus([]uint{article.ID}, articleUserModel.ID) asserts.Equal(true, statusMap[article.ID], "Should return true for favorited article") } func TestSetTagsRaceCondition(t *testing.T) { asserts := assert.New(t) user := createTestUser() articleUserModel := GetArticleUserModel(user) article := ArticleModel{ Slug: fmt.Sprintf("race-condition-article-%d", common.RandInt()), Title: "Race Condition Test", Description: "Test Description", Body: "Test Body", Author: articleUserModel, AuthorID: articleUserModel.ID, } // Test setTags with duplicate tags err := article.setTags([]string{"tag1", "tag2", "tag1"}) asserts.NoError(err, "setTags should handle duplicate tags") // Should have 2 unique tags asserts.Equal(3, len(article.Tags), "Should preserve all tags in list") } func TestArticleFeedWithEmptyFollowings(t *testing.T) { asserts := assert.New(t) user := createTestUser() articleUserModel := GetArticleUserModel(user) // Get feed with no followings articles, count, err := articleUserModel.GetArticleFeed("10", "0") asserts.NoError(err, "GetArticleFeed should succeed even with no followings") asserts.Equal(0, count, "Count should be 0 with no followings") asserts.NotNil(articles, "Articles should not be nil") } func TestArticleDeleteSuccess(t *testing.T) { asserts := assert.New(t) r := setupRouter() user := createTestUser() // Create an article articleUserModel := GetArticleUserModel(user) slug := fmt.Sprintf("delete-success-article-%d", common.RandInt()) article := ArticleModel{ Slug: slug, Title: "Delete Success Test", Description: "Test Description", Body: "Test Body", Author: articleUserModel, AuthorID: articleUserModel.ID, } SaveOne(&article) // Test delete existing article req, _ := http.NewRequest("DELETE", fmt.Sprintf("/api/articles/%s", slug), nil) common.HeaderTokenMock(req, user.ID) w := httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusOK, w.Code, "Delete existing article should return 200") // Verify article is deleted foundArticle, err := FindOneArticle(&ArticleModel{Slug: slug}) asserts.Error(err, "Article should not be found after deletion") asserts.Equal(uint(0), foundArticle.ID, "Article ID should be 0") } func TestTagListSuccess(t *testing.T) { asserts := assert.New(t) r := setupRouter() // Create some test tags tag1 := TagModel{Tag: fmt.Sprintf("listtag1-%d", common.RandInt())} tag2 := TagModel{Tag: fmt.Sprintf("listtag2-%d", common.RandInt())} test_db.Create(&tag1) test_db.Create(&tag2) // Test list tags endpoint req, _ := http.NewRequest("GET", "/api/tags", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusOK, w.Code, "Tags list should return 200") asserts.Contains(w.Body.String(), `"tags"`, "Response should contain tags") } func TestArticleListErrorHandling(t *testing.T) { asserts := assert.New(t) r := setupRouter() // Test with invalid limit/offset parameters req, _ := http.NewRequest("GET", "/api/articles?limit=abc&offset=xyz", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusOK, w.Code, "List with invalid params should still return 200") asserts.Contains(w.Body.String(), `"articles"`, "Response should contain articles array") } func TestArticleFeedErrorPath(t *testing.T) { asserts := assert.New(t) r := setupRouter() user := createTestUser() // Test with invalid limit/offset req, _ := http.NewRequest("GET", "/api/articles/feed?limit=invalid&offset=invalid", nil) common.HeaderTokenMock(req, user.ID) w := httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusOK, w.Code, "Feed with invalid params should return 200") asserts.Contains(w.Body.String(), `"articles"`, "Response should contain articles") } func TestArticleCreateValidation(t *testing.T) { asserts := assert.New(t) r := setupRouter() user := createTestUser() // Test with empty fields req, _ := http.NewRequest("POST", "/api/articles", bytes.NewBufferString(`{"article":{"title":"","description":"","body":""}}`)) req.Header.Set("Content-Type", "application/json") common.HeaderTokenMock(req, user.ID) w := httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusUnprocessableEntity, w.Code, "Empty fields should return 422") } func TestArticleUpdateNonExistent(t *testing.T) { asserts := assert.New(t) r := setupRouter() user := createTestUser() // Test update non-existent article req, _ := http.NewRequest("PUT", "/api/articles/non-existent-article", bytes.NewBufferString(`{"article":{"title":"New Title"}}`)) req.Header.Set("Content-Type", "application/json") common.HeaderTokenMock(req, user.ID) w := httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusNotFound, w.Code, "Update non-existent article should return 404") } func TestArticleDeleteAuthorizationForbidden(t *testing.T) { asserts := assert.New(t) r := setupRouter() user := createTestUser() otherUser := createTestUser() // Create article by user articleUserModel := GetArticleUserModel(user) slug := fmt.Sprintf("forbidden-delete-article-%d", common.RandInt()) article := ArticleModel{ Slug: slug, Title: "Forbidden Delete Article", Description: "Test Description", Body: "Test Body", Author: articleUserModel, AuthorID: articleUserModel.ID, } SaveOne(&article) // Try to delete by otherUser req, _ := http.NewRequest("DELETE", fmt.Sprintf("/api/articles/%s", slug), nil) common.HeaderTokenMock(req, otherUser.ID) w := httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusForbidden, w.Code, "Delete by non-author should return 403") // Verify article still exists foundArticle, err := FindOneArticle(&ArticleModel{Slug: slug}) asserts.NoError(err, "Article should still exist") asserts.Equal(article.ID, foundArticle.ID, "Article ID should match") } func TestArticleUpdateAuthorizationForbidden(t *testing.T) { asserts := assert.New(t) r := setupRouter() user := createTestUser() otherUser := createTestUser() // Create article by user articleUserModel := GetArticleUserModel(user) slug := fmt.Sprintf("forbidden-update-article-%d", common.RandInt()) title := "Forbidden Update Article" article := ArticleModel{ Slug: slug, Title: title, Description: "Test Description", Body: "Test Body", Author: articleUserModel, AuthorID: articleUserModel.ID, } SaveOne(&article) // Try to update by otherUser req, _ := http.NewRequest("PUT", fmt.Sprintf("/api/articles/%s", slug), bytes.NewBufferString(`{"article":{"title":"New Title"}}`)) req.Header.Set("Content-Type", "application/json") common.HeaderTokenMock(req, otherUser.ID) w := httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusForbidden, w.Code, "Update by non-author should return 403") // Verify article is unchanged foundArticle, _ := FindOneArticle(&ArticleModel{Slug: slug}) asserts.Equal(title, foundArticle.Title, "Article title should be unchanged") } func TestCommentDeleteAuthorizationForbidden(t *testing.T) { asserts := assert.New(t) r := setupRouter() user := createTestUser() otherUser := createTestUser() // Create article articleUserModel := GetArticleUserModel(user) slug := fmt.Sprintf("forbidden-comment-delete-%d", common.RandInt()) article := ArticleModel{ Slug: slug, Title: "Forbidden Comment Delete", Description: "Test Description", Body: "Test Body", Author: articleUserModel, AuthorID: articleUserModel.ID, } SaveOne(&article) // Create comment by user comment := CommentModel{ ArticleID: article.ID, AuthorID: articleUserModel.ID, Body: "Test comment", } test_db.Create(&comment) // Try to delete by otherUser req, _ := http.NewRequest("DELETE", fmt.Sprintf("/api/articles/%s/comments/%d", slug, comment.ID), nil) common.HeaderTokenMock(req, otherUser.ID) w := httptest.NewRecorder() r.ServeHTTP(w, req) asserts.Equal(http.StatusForbidden, w.Code, "Delete comment by non-author should return 403") // Verify comment still exists foundComment, err := FindOneComment(&CommentModel{Model: gorm.Model{ID: comment.ID}}) asserts.NoError(err, "Comment should still exist") asserts.Equal(comment.ID, foundComment.ID, "Comment ID should match") } // This is a hack way to add test database for each case func TestMain(m *testing.M) { test_db = common.TestDBInit() users.AutoMigrate() test_db.AutoMigrate(&ArticleModel{}) test_db.AutoMigrate(&TagModel{}) test_db.AutoMigrate(&FavoriteModel{}) test_db.AutoMigrate(&ArticleUserModel{}) test_db.AutoMigrate(&CommentModel{}) exitVal := m.Run() common.TestDBFree(test_db) os.Exit(exitVal) }