package forum import ( "os" "testing" "time" "dk/internal/database" ) func setupTestDB(t *testing.T) *database.DB { testDB := "test_forum.db" t.Cleanup(func() { os.Remove(testDB) }) db, err := database.Open(testDB) if err != nil { t.Fatalf("Failed to open test database: %v", err) } // Create forum table createTable := `CREATE TABLE forum ( id INTEGER PRIMARY KEY AUTOINCREMENT, posted INTEGER NOT NULL DEFAULT (unixepoch()), last_post INTEGER NOT NULL DEFAULT (unixepoch()), author INTEGER NOT NULL, parent INTEGER NOT NULL DEFAULT 0, replies INTEGER NOT NULL DEFAULT 0, title TEXT NOT NULL, content TEXT NOT NULL )` if err := db.Exec(createTable); err != nil { t.Fatalf("Failed to create forum table: %v", err) } // Insert test data with specific timestamps for predictable testing now := time.Now().Unix() testForum := `INSERT INTO forum (posted, last_post, author, parent, replies, title, content) VALUES (?, ?, 1, 0, 2, 'Welcome to the Game!', 'This is the first thread about our awesome game.'), (?, ?, 2, 1, 0, 'Re: Welcome to the Game!', 'Thanks! I am excited to start playing.'), (?, ?, 3, 1, 0, 'Re: Welcome to the Game!', 'Great game so far, loving the mechanics!'), (?, ?, 1, 0, 1, 'Bug Reports', 'Please report any bugs you find here.'), (?, ?, 2, 4, 0, 'Re: Bug Reports', 'Found a small issue with spell casting.'), (?, ?, 3, 0, 0, 'Strategy Discussion', 'Let us discuss optimal character builds and strategies.')` timestamps := []interface{}{ now - 86400*7, now - 86400*1, // Thread 1, last activity 1 day ago now - 86400*6, now - 86400*6, // Reply 1 now - 86400*1, now - 86400*1, // Reply 2 (most recent activity on thread 1) now - 86400*3, now - 86400*2, // Thread 2, last activity 2 days ago now - 86400*2, now - 86400*2, // Reply to thread 2 (most recent activity on thread 2) now - 3600*2, now - 3600*2, // Thread 3, 2 hours ago (most recent) } if err := db.Exec(testForum, timestamps...); err != nil { t.Fatalf("Failed to insert test forum data: %v", err) } return db } func TestFind(t *testing.T) { db := setupTestDB(t) defer db.Close() // Test finding existing forum post post, err := Find(db, 1) if err != nil { t.Fatalf("Failed to find forum post: %v", err) } if post.ID != 1 { t.Errorf("Expected ID 1, got %d", post.ID) } if post.Author != 1 { t.Errorf("Expected author 1, got %d", post.Author) } if post.Parent != 0 { t.Errorf("Expected parent 0, got %d", post.Parent) } if post.Replies != 2 { t.Errorf("Expected replies 2, got %d", post.Replies) } if post.Title != "Welcome to the Game!" { t.Errorf("Expected title 'Welcome to the Game!', got '%s'", post.Title) } if post.Content != "This is the first thread about our awesome game." { t.Errorf("Expected specific content, got '%s'", post.Content) } if post.Posted == 0 { t.Error("Expected non-zero posted timestamp") } if post.LastPost == 0 { t.Error("Expected non-zero last_post timestamp") } // Test finding non-existent forum post _, err = Find(db, 999) if err == nil { t.Error("Expected error when finding non-existent forum post") } } func TestAll(t *testing.T) { db := setupTestDB(t) defer db.Close() posts, err := All(db) if err != nil { t.Fatalf("Failed to get all forum posts: %v", err) } if len(posts) != 6 { t.Errorf("Expected 6 forum posts, got %d", len(posts)) } // Check ordering (by last_post DESC) if len(posts) >= 2 { if posts[0].LastPost < posts[1].LastPost { t.Error("Expected posts to be ordered by last_post (newest first)") } } // First post should be the most recent activity (2 hours ago) if posts[0].Title != "Strategy Discussion" { t.Errorf("Expected newest activity to be 'Strategy Discussion', got '%s'", posts[0].Title) } } func TestThreads(t *testing.T) { db := setupTestDB(t) defer db.Close() threads, err := Threads(db) if err != nil { t.Fatalf("Failed to get forum threads: %v", err) } if len(threads) != 3 { t.Errorf("Expected 3 threads, got %d", len(threads)) } // Verify all are threads (parent = 0) for _, thread := range threads { if thread.Parent != 0 { t.Errorf("Expected thread to have parent 0, got %d", thread.Parent) } if !thread.IsThread() { t.Errorf("Expected IsThread() to return true for thread %d", thread.ID) } if thread.IsReply() { t.Errorf("Expected IsReply() to return false for thread %d", thread.ID) } } // Check ordering (by last_post DESC) if len(threads) >= 2 { if threads[0].LastPost < threads[1].LastPost { t.Error("Expected threads to be ordered by last activity") } } } func TestByParent(t *testing.T) { db := setupTestDB(t) defer db.Close() // Test replies to thread 1 replies, err := ByParent(db, 1) if err != nil { t.Fatalf("Failed to get replies: %v", err) } if len(replies) != 2 { t.Errorf("Expected 2 replies to thread 1, got %d", len(replies)) } // Verify all are replies to thread 1 for _, reply := range replies { if reply.Parent != 1 { t.Errorf("Expected reply to have parent 1, got %d", reply.Parent) } if !reply.IsReply() { t.Errorf("Expected IsReply() to return true for reply %d", reply.ID) } if reply.IsThread() { t.Errorf("Expected IsThread() to return false for reply %d", reply.ID) } } // Check ordering (by posted ASC for replies) if len(replies) == 2 { if replies[0].Posted > replies[1].Posted { t.Error("Expected replies to be ordered by posted time (oldest first)") } } // Test no replies case noReplies, err := ByParent(db, 6) // Thread 3 has no replies if err != nil { t.Fatalf("Failed to get replies for thread with no replies: %v", err) } if len(noReplies) != 0 { t.Errorf("Expected 0 replies for thread 3, got %d", len(noReplies)) } } func TestByAuthor(t *testing.T) { db := setupTestDB(t) defer db.Close() // Test posts by author 1 author1Posts, err := ByAuthor(db, 1) if err != nil { t.Fatalf("Failed to get posts by author: %v", err) } if len(author1Posts) != 2 { t.Errorf("Expected 2 posts by author 1, got %d", len(author1Posts)) } // Verify all posts are by author 1 for _, post := range author1Posts { if post.Author != 1 { t.Errorf("Expected author 1, got %d", post.Author) } } // Check ordering (by posted DESC) if len(author1Posts) == 2 { if author1Posts[0].Posted < author1Posts[1].Posted { t.Error("Expected posts to be ordered by posted time (newest first)") } } // Test author with no posts noPosts, err := ByAuthor(db, 999) if err != nil { t.Fatalf("Failed to query non-existent author: %v", err) } if len(noPosts) != 0 { t.Errorf("Expected 0 posts by non-existent author, got %d", len(noPosts)) } } func TestRecent(t *testing.T) { db := setupTestDB(t) defer db.Close() // Test getting 3 most recent posts recentPosts, err := Recent(db, 3) if err != nil { t.Fatalf("Failed to get recent forum posts: %v", err) } if len(recentPosts) != 3 { t.Errorf("Expected 3 recent posts, got %d", len(recentPosts)) } // Check ordering (by last_post DESC) if len(recentPosts) >= 2 { if recentPosts[0].LastPost < recentPosts[1].LastPost { t.Error("Expected recent posts to be ordered by last activity") } } // Test getting more posts than exist allRecentPosts, err := Recent(db, 10) if err != nil { t.Fatalf("Failed to get recent posts with high limit: %v", err) } if len(allRecentPosts) != 6 { t.Errorf("Expected 6 posts (all available), got %d", len(allRecentPosts)) } } func TestSearch(t *testing.T) { db := setupTestDB(t) defer db.Close() // Test searching for "game" gamePosts, err := Search(db, "game") if err != nil { t.Fatalf("Failed to search forum posts: %v", err) } expectedCount := 3 // Welcome thread title, content, and replies containing "game" if len(gamePosts) != expectedCount { t.Errorf("Expected %d posts containing 'game', got %d", expectedCount, len(gamePosts)) } // Verify all posts contain the search term for _, post := range gamePosts { if !post.Contains("game") { t.Errorf("Post '%s' does not contain search term 'game'", post.Title) } } // Test case insensitive search gamePostsUpper, err := Search(db, "GAME") if err != nil { t.Fatalf("Failed to search with uppercase: %v", err) } if len(gamePostsUpper) != expectedCount { t.Error("Expected case insensitive search to find same results") } // Test search with no results noResults, err := Search(db, "nonexistentterm") if err != nil { t.Fatalf("Failed to search for non-existent term: %v", err) } if len(noResults) != 0 { t.Errorf("Expected 0 results for non-existent term, got %d", len(noResults)) } } func TestSince(t *testing.T) { db := setupTestDB(t) defer db.Close() // Test posts with activity since 3 days ago threeDaysAgo := time.Now().AddDate(0, 0, -3).Unix() recentPosts, err := Since(db, threeDaysAgo) if err != nil { t.Fatalf("Failed to get posts since timestamp: %v", err) } // Should get posts with last_post within last 3 days (includes replies) expectedCount := 5 // Thread 1 (1 day ago), Reply 2 to Thread 1, Thread 2 (2 days ago), Reply to Thread 2, Thread 3 (2 hours ago) if len(recentPosts) != expectedCount { t.Errorf("Expected %d posts with activity since 3 days ago, got %d", expectedCount, len(recentPosts)) } // Verify all posts have last_post since the timestamp for _, post := range recentPosts { if post.LastPost < threeDaysAgo { t.Errorf("Post with last_post %d is before the 'since' timestamp %d", post.LastPost, threeDaysAgo) } } // Test with future timestamp (should return no posts) futurePosts, err := Since(db, time.Now().Add(time.Hour).Unix()) if err != nil { t.Fatalf("Failed to query future timestamp: %v", err) } if len(futurePosts) != 0 { t.Errorf("Expected 0 posts since future timestamp, got %d", len(futurePosts)) } } func TestBuilder(t *testing.T) { db := setupTestDB(t) defer db.Close() // Create new thread using builder testTime := time.Now() post, err := NewBuilder(db). WithAuthor(5). WithTitle("Test Thread"). WithContent("This is a test thread created with the builder"). WithPostedTime(testTime). WithLastPostTime(testTime). AsThread(). Create() if err != nil { t.Fatalf("Failed to create forum post with builder: %v", err) } if post.ID == 0 { t.Error("Expected non-zero ID after creation") } if post.Author != 5 { t.Errorf("Expected author 5, got %d", post.Author) } if post.Title != "Test Thread" { t.Errorf("Expected title 'Test Thread', got '%s'", post.Title) } if post.Content != "This is a test thread created with the builder" { t.Errorf("Expected specific content, got '%s'", post.Content) } if post.Posted != testTime.Unix() { t.Errorf("Expected posted time %d, got %d", testTime.Unix(), post.Posted) } if post.Parent != 0 { t.Errorf("Expected parent 0 (thread), got %d", post.Parent) } if post.Replies != 0 { t.Errorf("Expected replies 0, got %d", post.Replies) } // Create reply using builder reply, err := NewBuilder(db). WithAuthor(6). WithTitle("Re: Test Thread"). WithContent("This is a reply to the test thread"). AsReply(post.ID). Create() if err != nil { t.Fatalf("Failed to create reply with builder: %v", err) } if reply.Parent != post.ID { t.Errorf("Expected parent %d, got %d", post.ID, reply.Parent) } if !reply.IsReply() { t.Error("Expected reply to be identified as reply") } // Verify posts were saved to database foundPost, err := Find(db, post.ID) if err != nil { t.Fatalf("Failed to find created post: %v", err) } if foundPost.Title != "Test Thread" { t.Errorf("Created post not found in database") } // Test builder with default timestamp defaultPost, err := NewBuilder(db). WithAuthor(7). WithTitle("Default Time Post"). WithContent("Post with default timestamps"). Create() if err != nil { t.Fatalf("Failed to create post with default timestamp: %v", err) } // Should have recent timestamps (within last minute) if time.Since(defaultPost.PostedTime()) > time.Minute { t.Error("Expected default posted timestamp to be recent") } if time.Since(defaultPost.LastPostTime()) > time.Minute { t.Error("Expected default last_post timestamp to be recent") } } func TestSave(t *testing.T) { db := setupTestDB(t) defer db.Close() post, err := Find(db, 1) if err != nil { t.Fatalf("Failed to find forum post: %v", err) } // Modify post post.Title = "Updated Welcome Thread" post.Content = "This content has been updated by moderator" post.Replies = 3 post.UpdateLastPost() // Save changes err = post.Save() if err != nil { t.Fatalf("Failed to save forum post: %v", err) } // Verify changes were saved updatedPost, err := Find(db, 1) if err != nil { t.Fatalf("Failed to find updated post: %v", err) } if updatedPost.Title != "Updated Welcome Thread" { t.Errorf("Expected updated title 'Updated Welcome Thread', got '%s'", updatedPost.Title) } if updatedPost.Content != "This content has been updated by moderator" { t.Errorf("Expected updated content, got '%s'", updatedPost.Content) } if updatedPost.Replies != 3 { t.Errorf("Expected updated replies 3, got %d", updatedPost.Replies) } } func TestDelete(t *testing.T) { db := setupTestDB(t) defer db.Close() post, err := Find(db, 1) if err != nil { t.Fatalf("Failed to find forum post: %v", err) } // Delete post err = post.Delete() if err != nil { t.Fatalf("Failed to delete forum post: %v", err) } // Verify post was deleted _, err = Find(db, 1) if err == nil { t.Error("Expected error when finding deleted post") } } func TestUtilityMethods(t *testing.T) { db := setupTestDB(t) defer db.Close() post, _ := Find(db, 1) // Test time methods postedTime := post.PostedTime() if postedTime.IsZero() { t.Error("Expected non-zero posted time") } lastPostTime := post.LastPostTime() if lastPostTime.IsZero() { t.Error("Expected non-zero last post time") } // Test SetPostedTime and SetLastPostTime newTime := time.Now().Add(-2 * time.Hour) post.SetPostedTime(newTime) post.SetLastPostTime(newTime) if post.Posted != newTime.Unix() { t.Errorf("Expected posted timestamp %d, got %d", newTime.Unix(), post.Posted) } if post.LastPost != newTime.Unix() { t.Errorf("Expected last_post timestamp %d, got %d", newTime.Unix(), post.LastPost) } // Test activity age methods activityAge := post.ActivityAge() if activityAge < 0 { t.Error("Expected positive activity age") } postAge := post.PostAge() if postAge < 0 { t.Error("Expected positive post age") } // Test IsRecentActivity post.UpdateLastPost() // Set to now if !post.IsRecentActivity() { t.Error("Expected post with current timestamp to have recent activity") } // Test IsAuthor if !post.IsAuthor(post.Author) { t.Error("Expected IsAuthor to return true for correct author") } if post.IsAuthor(999) { t.Error("Expected IsAuthor to return false for incorrect author") } // Test HasReplies if !post.HasReplies() { t.Error("Expected post with replies > 0 to HasReplies") } // Test Preview longContent := "This is a very long forum post content that should be truncated when preview is called for display purposes" post.Content = longContent preview := post.Preview(20) if len(preview) > 20 { t.Errorf("Expected preview length <= 20, got %d", len(preview)) } if preview[len(preview)-3:] != "..." { t.Error("Expected preview to end with ellipsis") } shortPreview := post.Preview(200) // Longer than content if shortPreview != longContent { t.Error("Expected short content to not be truncated") } // Test WordCount post.Content = "This is a test with five words" wordCount := post.WordCount() if wordCount != 7 { t.Errorf("Expected 7 words, got %d", wordCount) } // Test Length expectedLength := len(post.Content) if post.Length() != expectedLength { t.Errorf("Expected length %d, got %d", expectedLength, post.Length()) } // Test Contains if !post.Contains("test") { t.Error("Expected post to contain 'test'") } if !post.Contains("TEST") { // Case insensitive t.Error("Expected Contains to be case insensitive") } if post.Contains("nonexistent") { t.Error("Expected post not to contain 'nonexistent'") } // Test reply count methods originalReplies := post.Replies post.IncrementReplies() if post.Replies != originalReplies+1 { t.Errorf("Expected replies to be incremented to %d, got %d", originalReplies+1, post.Replies) } post.DecrementReplies() if post.Replies != originalReplies { t.Errorf("Expected replies to be decremented back to %d, got %d", originalReplies, post.Replies) } // Test DecrementReplies with 0 replies post.Replies = 0 post.DecrementReplies() if post.Replies != 0 { t.Errorf("Expected replies to stay at 0 when decrementing from 0, got %d", post.Replies) } } func TestRelationshipMethods(t *testing.T) { db := setupTestDB(t) defer db.Close() // Test GetReplies on a thread thread, _ := Find(db, 1) // Thread with 2 replies replies, err := thread.GetReplies() if err != nil { t.Fatalf("Failed to get replies: %v", err) } if len(replies) != 2 { t.Errorf("Expected 2 replies, got %d", len(replies)) } // Test GetThread on a reply reply, _ := Find(db, 2) // Reply to thread 1 parentThread, err := reply.GetThread() if err != nil { t.Fatalf("Failed to get parent thread: %v", err) } if parentThread.ID != 1 { t.Errorf("Expected parent thread ID 1, got %d", parentThread.ID) } // Test GetThread on a thread (should return self) threadSelf, err := thread.GetThread() if err != nil { t.Fatalf("Failed to get thread (self): %v", err) } if threadSelf.ID != thread.ID { t.Errorf("Expected GetThread on thread to return self, got ID %d", threadSelf.ID) } }