diff --git a/internal/babble/babble_test.go b/internal/babble/babble_test.go index b9d4e7b..27b7aa4 100644 --- a/internal/babble/babble_test.go +++ b/internal/babble/babble_test.go @@ -13,12 +13,12 @@ func setupTestDB(t *testing.T) *database.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 babble table createTable := `CREATE TABLE babble ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -26,11 +26,11 @@ func setupTestDB(t *testing.T) *database.DB { author TEXT NOT NULL DEFAULT '', babble TEXT NOT NULL DEFAULT '' )` - + if err := db.Exec(createTable); err != nil { t.Fatalf("Failed to create babble table: %v", err) } - + // Insert test data with specific timestamps for predictable testing now := time.Now().Unix() testBabble := `INSERT INTO babble (posted, author, babble) VALUES @@ -40,33 +40,33 @@ func setupTestDB(t *testing.T) *database.DB { (?, 'Alice', 'I can help @Charlie, let me know'), (?, 'David', 'Server lag is really bad right now...'), (?, 'Eve', 'Quick question about spell mechanics')` - - timestamps := []interface{}{ + + timestamps := []any{ now - 3600*6, // 6 hours ago - now - 3600*4, // 4 hours ago + now - 3600*4, // 4 hours ago now - 3600*2, // 2 hours ago now - 3600*1, // 1 hour ago now - 1800, // 30 minutes ago now - 300, // 5 minutes ago } - + if err := db.Exec(testBabble, timestamps...); err != nil { t.Fatalf("Failed to insert test babble: %v", err) } - + return db } func TestFind(t *testing.T) { db := setupTestDB(t) defer db.Close() - + // Test finding existing babble babble, err := Find(db, 1) if err != nil { t.Fatalf("Failed to find babble: %v", err) } - + if babble.ID != 1 { t.Errorf("Expected ID 1, got %d", babble.ID) } @@ -79,7 +79,7 @@ func TestFind(t *testing.T) { if babble.Posted == 0 { t.Error("Expected non-zero posted timestamp") } - + // Test finding non-existent babble _, err = Find(db, 999) if err == nil { @@ -90,23 +90,23 @@ func TestFind(t *testing.T) { func TestAll(t *testing.T) { db := setupTestDB(t) defer db.Close() - + babbles, err := All(db) if err != nil { t.Fatalf("Failed to get all babble: %v", err) } - + if len(babbles) != 6 { t.Errorf("Expected 6 babble messages, got %d", len(babbles)) } - + // Check ordering (newest first) if len(babbles) >= 2 { if babbles[0].Posted < babbles[1].Posted { t.Error("Expected babble to be ordered by posted time (newest first)") } } - + // First message should be the most recent (5 minutes ago) if babbles[0].Author != "Eve" { t.Errorf("Expected newest message from Eve, got from '%s'", babbles[0].Author) @@ -116,47 +116,47 @@ func TestAll(t *testing.T) { func TestByAuthor(t *testing.T) { db := setupTestDB(t) defer db.Close() - + // Test messages by Alice aliceMessages, err := ByAuthor(db, "Alice") if err != nil { t.Fatalf("Failed to get babble by author: %v", err) } - + if len(aliceMessages) != 2 { t.Errorf("Expected 2 messages by Alice, got %d", len(aliceMessages)) } - + // Verify all messages are by Alice for _, message := range aliceMessages { if message.Author != "Alice" { t.Errorf("Expected author 'Alice', got '%s'", message.Author) } } - + // Check ordering (newest first) if len(aliceMessages) == 2 { if aliceMessages[0].Babble != "I can help @Charlie, let me know" { t.Errorf("Expected newest message by Alice first") } } - + // Test case insensitive search aliceMessagesLower, err := ByAuthor(db, "alice") if err != nil { t.Fatalf("Failed to get babble by lowercase author: %v", err) } - + if len(aliceMessagesLower) != 2 { t.Errorf("Expected case insensitive search to find 2 messages, got %d", len(aliceMessagesLower)) } - + // Test author with no messages noMessages, err := ByAuthor(db, "NonexistentUser") if err != nil { t.Fatalf("Failed to query non-existent author: %v", err) } - + if len(noMessages) != 0 { t.Errorf("Expected 0 messages by non-existent author, got %d", len(noMessages)) } @@ -165,30 +165,30 @@ func TestByAuthor(t *testing.T) { func TestRecent(t *testing.T) { db := setupTestDB(t) defer db.Close() - + // Test getting 3 most recent messages recentMessages, err := Recent(db, 3) if err != nil { t.Fatalf("Failed to get recent babble: %v", err) } - + if len(recentMessages) != 3 { t.Errorf("Expected 3 recent messages, got %d", len(recentMessages)) } - + // Check ordering (newest first) if len(recentMessages) >= 2 { if recentMessages[0].Posted < recentMessages[1].Posted { t.Error("Expected recent messages to be ordered newest first") } } - + // Test getting more messages than exist allRecentMessages, err := Recent(db, 10) if err != nil { t.Fatalf("Failed to get recent babble with high limit: %v", err) } - + if len(allRecentMessages) != 6 { t.Errorf("Expected 6 messages (all available), got %d", len(allRecentMessages)) } @@ -197,33 +197,33 @@ func TestRecent(t *testing.T) { func TestSince(t *testing.T) { db := setupTestDB(t) defer db.Close() - + // Test messages since 3 hours ago threeHoursAgo := time.Now().Add(-3 * time.Hour).Unix() recentMessages, err := Since(db, threeHoursAgo) if err != nil { t.Fatalf("Failed to get babble since timestamp: %v", err) } - + // Should get messages from 2 hours ago, 1 hour ago, 30 minutes ago, and 5 minutes ago expectedCount := 4 if len(recentMessages) != expectedCount { t.Errorf("Expected %d messages since 3 hours ago, got %d", expectedCount, len(recentMessages)) } - + // Verify all messages are since the timestamp for _, message := range recentMessages { if message.Posted < threeHoursAgo { t.Errorf("Message with timestamp %d is before the 'since' timestamp %d", message.Posted, threeHoursAgo) } } - + // Test with future timestamp (should return no messages) futureMessages, err := Since(db, time.Now().Add(time.Hour).Unix()) if err != nil { t.Fatalf("Failed to query future timestamp: %v", err) } - + if len(futureMessages) != 0 { t.Errorf("Expected 0 messages since future timestamp, got %d", len(futureMessages)) } @@ -232,22 +232,22 @@ func TestSince(t *testing.T) { func TestBetween(t *testing.T) { db := setupTestDB(t) defer db.Close() - + // Test messages between 5 hours ago and 1 hour ago start := time.Now().Add(-5 * time.Hour).Unix() end := time.Now().Add(-1 * time.Hour).Unix() - + betweenMessages, err := Between(db, start, end) if err != nil { t.Fatalf("Failed to get babble between timestamps: %v", err) } - + // Should get messages from 4 hours ago, 2 hours ago, and 1 hour ago (inclusive end) expectedCount := 3 if len(betweenMessages) != expectedCount { t.Errorf("Expected %d messages between timestamps, got %d", expectedCount, len(betweenMessages)) } - + // Verify all messages are within the range for _, message := range betweenMessages { if message.Posted < start || message.Posted > end { @@ -259,41 +259,41 @@ func TestBetween(t *testing.T) { func TestSearch(t *testing.T) { db := setupTestDB(t) defer db.Close() - + // Test searching for "game" gameMessages, err := Search(db, "game") if err != nil { t.Fatalf("Failed to search babble: %v", err) } - + expectedCount := 2 // Alice's welcome message and Bob's response if len(gameMessages) != expectedCount { t.Errorf("Expected %d messages containing 'game', got %d", expectedCount, len(gameMessages)) } - + // Verify all messages contain the search term for _, message := range gameMessages { if !message.Contains("game") { t.Errorf("Message '%s' does not contain search term 'game'", message.Babble) } } - + // Test case insensitive search gameMessagesUpper, err := Search(db, "GAME") if err != nil { t.Fatalf("Failed to search babble with uppercase: %v", err) } - + if len(gameMessagesUpper) != 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)) } @@ -302,27 +302,27 @@ func TestSearch(t *testing.T) { func TestRecentByAuthor(t *testing.T) { db := setupTestDB(t) defer db.Close() - + // Test recent messages by Alice (limit 1) aliceRecent, err := RecentByAuthor(db, "Alice", 1) if err != nil { t.Fatalf("Failed to get recent babble by author: %v", err) } - + if len(aliceRecent) != 1 { t.Errorf("Expected 1 recent message by Alice, got %d", len(aliceRecent)) } - + if len(aliceRecent) > 0 && aliceRecent[0].Babble != "I can help @Charlie, let me know" { t.Error("Expected most recent message by Alice") } - + // Test with higher limit aliceAll, err := RecentByAuthor(db, "Alice", 5) if err != nil { t.Fatalf("Failed to get all recent messages by Alice: %v", err) } - + if len(aliceAll) != 2 { t.Errorf("Expected 2 total messages by Alice, got %d", len(aliceAll)) } @@ -331,7 +331,7 @@ func TestRecentByAuthor(t *testing.T) { func TestBuilder(t *testing.T) { db := setupTestDB(t) defer db.Close() - + // Create new babble using builder testTime := time.Now() babble, err := NewBuilder(db). @@ -339,11 +339,11 @@ func TestBuilder(t *testing.T) { WithBabble("Test message from builder"). WithPostedTime(testTime). Create() - + if err != nil { t.Fatalf("Failed to create babble with builder: %v", err) } - + if babble.ID == 0 { t.Error("Expected non-zero ID after creation") } @@ -356,41 +356,41 @@ func TestBuilder(t *testing.T) { if babble.Posted != testTime.Unix() { t.Errorf("Expected posted time %d, got %d", testTime.Unix(), babble.Posted) } - + // Test WithMessage alias babble2, err := NewBuilder(db). WithAuthor("TestUser2"). WithMessage("Using WithMessage alias"). Create() - + if err != nil { t.Fatalf("Failed to create babble with WithMessage: %v", err) } - + if babble2.Babble != "Using WithMessage alias" { t.Errorf("WithMessage alias failed, got '%s'", babble2.Babble) } - + // Verify it was saved to database foundBabble, err := Find(db, babble.ID) if err != nil { t.Fatalf("Failed to find created babble: %v", err) } - + if foundBabble.Babble != "Test message from builder" { t.Errorf("Created babble not found in database") } - + // Test builder with default timestamp defaultBabble, err := NewBuilder(db). WithAuthor("DefaultUser"). WithBabble("Message with default timestamp"). Create() - + if err != nil { t.Fatalf("Failed to create babble with default timestamp: %v", err) } - + // Should have recent timestamp (within last minute) if time.Since(defaultBabble.PostedTime()) > time.Minute { t.Error("Expected default timestamp to be recent") @@ -400,29 +400,29 @@ func TestBuilder(t *testing.T) { func TestSave(t *testing.T) { db := setupTestDB(t) defer db.Close() - + babble, err := Find(db, 1) if err != nil { t.Fatalf("Failed to find babble: %v", err) } - + // Modify babble babble.Author = "UpdatedAuthor" babble.Babble = "Updated message content" babble.Posted = time.Now().Unix() - + // Save changes err = babble.Save() if err != nil { t.Fatalf("Failed to save babble: %v", err) } - + // Verify changes were saved updatedBabble, err := Find(db, 1) if err != nil { t.Fatalf("Failed to find updated babble: %v", err) } - + if updatedBabble.Author != "UpdatedAuthor" { t.Errorf("Expected updated author 'UpdatedAuthor', got '%s'", updatedBabble.Author) } @@ -434,18 +434,18 @@ func TestSave(t *testing.T) { func TestDelete(t *testing.T) { db := setupTestDB(t) defer db.Close() - + babble, err := Find(db, 1) if err != nil { t.Fatalf("Failed to find babble: %v", err) } - + // Delete babble err = babble.Delete() if err != nil { t.Fatalf("Failed to delete babble: %v", err) } - + // Verify babble was deleted _, err = Find(db, 1) if err == nil { @@ -456,33 +456,33 @@ func TestDelete(t *testing.T) { func TestUtilityMethods(t *testing.T) { db := setupTestDB(t) defer db.Close() - + babble, _ := Find(db, 1) - + // Test PostedTime postedTime := babble.PostedTime() if postedTime.IsZero() { t.Error("Expected non-zero posted time") } - + // Test SetPostedTime newTime := time.Now().Add(-30 * time.Minute) babble.SetPostedTime(newTime) if babble.Posted != newTime.Unix() { t.Errorf("Expected posted timestamp %d, got %d", newTime.Unix(), babble.Posted) } - + // Test IsRecent (should be true for 30 minutes ago) if !babble.IsRecent() { t.Error("Expected message from 30 minutes ago to be recent") } - + // Test Age age := babble.Age() if age < 0 { t.Error("Expected positive age") } - + // Test IsAuthor if !babble.IsAuthor("Alice") { t.Error("Expected IsAuthor to return true for correct author") @@ -493,11 +493,11 @@ func TestUtilityMethods(t *testing.T) { if babble.IsAuthor("Bob") { t.Error("Expected IsAuthor to return false for incorrect author") } - + // Test Preview longMessage := "This is a very long chat message that should be truncated when preview is called for display purposes" babble.Babble = longMessage - + preview := babble.Preview(20) if len(preview) > 20 { t.Errorf("Expected preview length <= 20, got %d", len(preview)) @@ -505,25 +505,25 @@ func TestUtilityMethods(t *testing.T) { if preview[len(preview)-3:] != "..." { t.Error("Expected preview to end with ellipsis") } - + shortPreview := babble.Preview(200) // Longer than message if shortPreview != longMessage { t.Error("Expected short message to not be truncated") } - + // Test WordCount babble.Babble = "This is a test with five words" wordCount := babble.WordCount() if wordCount != 7 { t.Errorf("Expected 7 words, got %d", wordCount) } - + // Test Length expectedLength := len(babble.Babble) if babble.Length() != expectedLength { t.Errorf("Expected length %d, got %d", expectedLength, babble.Length()) } - + // Test Contains if !babble.Contains("test") { t.Error("Expected message to contain 'test'") @@ -534,23 +534,23 @@ func TestUtilityMethods(t *testing.T) { if babble.Contains("nonexistent") { t.Error("Expected message not to contain 'nonexistent'") } - + // Test IsEmpty babble.Babble = "" if !babble.IsEmpty() { t.Error("Expected empty message to be empty") } - + babble.Babble = " " if !babble.IsEmpty() { t.Error("Expected whitespace-only message to be empty") } - + babble.Babble = "Not empty" if babble.IsEmpty() { t.Error("Expected non-empty message not to be empty") } - + // Test IsLongMessage shortMsg := "Short" babble.Babble = shortMsg @@ -565,22 +565,22 @@ func TestUtilityMethods(t *testing.T) { func TestMentionMethods(t *testing.T) { db := setupTestDB(t) defer db.Close() - + // Test GetMentions babble, _ := Find(db, 2) // Bob's message: "Thanks Alice! @Alice this game is great" mentions := babble.GetMentions() - + expectedMentions := []string{"Alice"} if len(mentions) != len(expectedMentions) { t.Errorf("Expected %d mentions, got %d", len(expectedMentions), len(mentions)) } - + for i, expected := range expectedMentions { if i < len(mentions) && mentions[i] != expected { t.Errorf("Expected mention '%s' at position %d, got '%s'", expected, i, mentions[i]) } } - + // Test HasMention if !babble.HasMention("Alice") { t.Error("Expected message to mention Alice") @@ -591,35 +591,35 @@ func TestMentionMethods(t *testing.T) { if babble.HasMention("Bob") { t.Error("Expected message not to mention Bob") } - + // Test message with multiple mentions and punctuation babble.Babble = "Hey @Alice, @Bob! Can you help @Charlie?" mentions = babble.GetMentions() expectedMentions = []string{"Alice", "Bob", "Charlie"} - + if len(mentions) != len(expectedMentions) { t.Errorf("Expected %d mentions, got %d: %v", len(expectedMentions), len(mentions), mentions) } - + for _, expected := range expectedMentions { if !babble.HasMention(expected) { t.Errorf("Expected message to mention %s", expected) } } - + // Test message with no mentions babble.Babble = "No mentions in this message" mentions = babble.GetMentions() - + if len(mentions) != 0 { t.Errorf("Expected 0 mentions, got %d", len(mentions)) } - + // Test malformed mentions (should be ignored) babble.Babble = "Just @ alone or @" mentions = babble.GetMentions() - + if len(mentions) != 0 { t.Errorf("Expected 0 mentions for malformed @, got %d", len(mentions)) } -} \ No newline at end of file +} diff --git a/internal/forum/forum_test.go b/internal/forum/forum_test.go index 0561d19..e983ac0 100644 --- a/internal/forum/forum_test.go +++ b/internal/forum/forum_test.go @@ -45,13 +45,13 @@ func setupTestDB(t *testing.T) *database.DB { (?, ?, 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{}{ + timestamps := []any{ 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) + now - 3600*2, now - 3600*2, // Thread 3, 2 hours ago (most recent) } if err := db.Exec(testForum, timestamps...); err != nil { @@ -662,4 +662,4 @@ func TestRelationshipMethods(t *testing.T) { if threadSelf.ID != thread.ID { t.Errorf("Expected GetThread on thread to return self, got ID %d", threadSelf.ID) } -} \ No newline at end of file +} diff --git a/internal/news/news_test.go b/internal/news/news_test.go index 76ecc57..37ef284 100644 --- a/internal/news/news_test.go +++ b/internal/news/news_test.go @@ -13,12 +13,12 @@ func setupTestDB(t *testing.T) *database.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 news table createTable := `CREATE TABLE news ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -26,11 +26,11 @@ func setupTestDB(t *testing.T) *database.DB { posted INTEGER NOT NULL DEFAULT (unixepoch()), content TEXT NOT NULL )` - + if err := db.Exec(createTable); err != nil { t.Fatalf("Failed to create news table: %v", err) } - + // Insert test data with specific timestamps for predictable testing now := time.Now().Unix() testNews := `INSERT INTO news (author, posted, content) VALUES @@ -39,32 +39,32 @@ func setupTestDB(t *testing.T) *database.DB { (1, ?, 'Third post - recent update'), (3, ?, 'Fourth post from admin'), (2, ?, 'Fifth post - maintenance notice')` - - timestamps := []interface{}{ + + timestamps := []any{ now - 86400*7, // 1 week ago - now - 86400*5, // 5 days ago + now - 86400*5, // 5 days ago now - 86400*2, // 2 days ago now - 86400*1, // 1 day ago now - 3600, // 1 hour ago } - + if err := db.Exec(testNews, timestamps...); err != nil { t.Fatalf("Failed to insert test news: %v", err) } - + return db } func TestFind(t *testing.T) { db := setupTestDB(t) defer db.Close() - + // Test finding existing news news, err := Find(db, 1) if err != nil { t.Fatalf("Failed to find news: %v", err) } - + if news.ID != 1 { t.Errorf("Expected ID 1, got %d", news.ID) } @@ -77,7 +77,7 @@ func TestFind(t *testing.T) { if news.Posted == 0 { t.Error("Expected non-zero posted timestamp") } - + // Test finding non-existent news _, err = Find(db, 999) if err == nil { @@ -88,23 +88,23 @@ func TestFind(t *testing.T) { func TestAll(t *testing.T) { db := setupTestDB(t) defer db.Close() - + newsPosts, err := All(db) if err != nil { t.Fatalf("Failed to get all news: %v", err) } - + if len(newsPosts) != 5 { t.Errorf("Expected 5 news posts, got %d", len(newsPosts)) } - + // Check ordering (newest first) if len(newsPosts) >= 2 { if newsPosts[0].Posted < newsPosts[1].Posted { t.Error("Expected news to be ordered by posted time (newest first)") } } - + // First post should be the most recent (1 hour ago) if newsPosts[0].Content != "Fifth post - maintenance notice" { t.Errorf("Expected newest post first, got '%s'", newsPosts[0].Content) @@ -114,37 +114,37 @@ func TestAll(t *testing.T) { 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 news 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 (newest first) if len(author1Posts) == 2 { if author1Posts[0].Content != "Third post - recent update" { t.Errorf("Expected newest post by author 1 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)) } @@ -153,30 +153,30 @@ func TestByAuthor(t *testing.T) { 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 news: %v", err) } - + if len(recentPosts) != 3 { t.Errorf("Expected 3 recent posts, got %d", len(recentPosts)) } - + // Check ordering (newest first) if len(recentPosts) >= 2 { if recentPosts[0].Posted < recentPosts[1].Posted { t.Error("Expected recent posts to be ordered newest first") } } - + // Test getting more posts than exist allRecentPosts, err := Recent(db, 10) if err != nil { t.Fatalf("Failed to get recent news with high limit: %v", err) } - + if len(allRecentPosts) != 5 { t.Errorf("Expected 5 posts (all available), got %d", len(allRecentPosts)) } @@ -185,33 +185,33 @@ func TestRecent(t *testing.T) { func TestSince(t *testing.T) { db := setupTestDB(t) defer db.Close() - + // Test posts 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 news since timestamp: %v", err) } - + // Should get posts from 2 days ago, 1 day ago, and 1 hour ago expectedCount := 3 if len(recentPosts) != expectedCount { t.Errorf("Expected %d posts since 3 days ago, got %d", expectedCount, len(recentPosts)) } - + // Verify all posts are since the timestamp for _, post := range recentPosts { if post.Posted < threeDaysAgo { t.Errorf("Post with timestamp %d is before the 'since' timestamp %d", post.Posted, threeDaysAgo) } } - + // Test with future timestamp (should return no posts) futurePosts, err := Since(db, time.Now().AddDate(0, 0, 1).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)) } @@ -220,38 +220,38 @@ func TestSince(t *testing.T) { func TestBetween(t *testing.T) { db := setupTestDB(t) defer db.Close() - + // Test posts between 6 days ago and 1 day ago start := time.Now().AddDate(0, 0, -6).Unix() end := time.Now().AddDate(0, 0, -1).Unix() - + betweenPosts, err := Between(db, start, end) if err != nil { t.Fatalf("Failed to get news between timestamps: %v", err) } - + // Should get posts from 5 days ago, 2 days ago, and 1 day ago expectedCount := 3 if len(betweenPosts) != expectedCount { t.Errorf("Expected %d posts between timestamps, got %d", expectedCount, len(betweenPosts)) } - + // Verify all posts are within the range for _, post := range betweenPosts { if post.Posted < start || post.Posted > end { t.Errorf("Post with timestamp %d is outside range [%d, %d]", post.Posted, start, end) } } - + // Test with narrow range (should return fewer posts) narrowStart := time.Now().AddDate(0, 0, -2).Unix() narrowEnd := time.Now().AddDate(0, 0, -1).Unix() - + narrowPosts, err := Between(db, narrowStart, narrowEnd) if err != nil { t.Fatalf("Failed to get news in narrow range: %v", err) } - + if len(narrowPosts) != 2 { // 2 days ago and 1 day ago t.Errorf("Expected 2 posts in narrow range, got %d", len(narrowPosts)) } @@ -260,7 +260,7 @@ func TestBetween(t *testing.T) { func TestBuilder(t *testing.T) { db := setupTestDB(t) defer db.Close() - + // Create new news using builder testTime := time.Now() news, err := NewBuilder(db). @@ -268,11 +268,11 @@ func TestBuilder(t *testing.T) { WithContent("Test news content from builder"). WithPostedTime(testTime). Create() - + if err != nil { t.Fatalf("Failed to create news with builder: %v", err) } - + if news.ID == 0 { t.Error("Expected non-zero ID after creation") } @@ -285,27 +285,27 @@ func TestBuilder(t *testing.T) { if news.Posted != testTime.Unix() { t.Errorf("Expected posted time %d, got %d", testTime.Unix(), news.Posted) } - + // Verify it was saved to database foundNews, err := Find(db, news.ID) if err != nil { t.Fatalf("Failed to find created news: %v", err) } - + if foundNews.Content != "Test news content from builder" { t.Errorf("Created news not found in database") } - + // Test builder with default timestamp defaultNews, err := NewBuilder(db). WithAuthor(1). WithContent("News with default timestamp"). Create() - + if err != nil { t.Fatalf("Failed to create news with default timestamp: %v", err) } - + // Should have recent timestamp (within last minute) if time.Since(defaultNews.PostedTime()) > time.Minute { t.Error("Expected default timestamp to be recent") @@ -315,29 +315,29 @@ func TestBuilder(t *testing.T) { func TestSave(t *testing.T) { db := setupTestDB(t) defer db.Close() - + news, err := Find(db, 1) if err != nil { t.Fatalf("Failed to find news: %v", err) } - + // Modify news news.Author = 999 news.Content = "Updated content" news.Posted = time.Now().Unix() - + // Save changes err = news.Save() if err != nil { t.Fatalf("Failed to save news: %v", err) } - + // Verify changes were saved updatedNews, err := Find(db, 1) if err != nil { t.Fatalf("Failed to find updated news: %v", err) } - + if updatedNews.Author != 999 { t.Errorf("Expected updated author 999, got %d", updatedNews.Author) } @@ -349,18 +349,18 @@ func TestSave(t *testing.T) { func TestDelete(t *testing.T) { db := setupTestDB(t) defer db.Close() - + news, err := Find(db, 1) if err != nil { t.Fatalf("Failed to find news: %v", err) } - + // Delete news err = news.Delete() if err != nil { t.Fatalf("Failed to delete news: %v", err) } - + // Verify news was deleted _, err = Find(db, 1) if err == nil { @@ -371,48 +371,48 @@ func TestDelete(t *testing.T) { func TestUtilityMethods(t *testing.T) { db := setupTestDB(t) defer db.Close() - + news, _ := Find(db, 1) - + // Test PostedTime postedTime := news.PostedTime() if postedTime.IsZero() { t.Error("Expected non-zero posted time") } - + // Test IsRecent (should be false for old posts initially) if news.IsRecent() { t.Error("Expected old news post not to be recent") } - + // Test SetPostedTime newTime := time.Now().Add(-2 * time.Hour) news.SetPostedTime(newTime) if news.Posted != newTime.Unix() { t.Errorf("Expected posted timestamp %d, got %d", newTime.Unix(), news.Posted) } - + // Test IsRecent (should be true after setting to 2 hours ago) if !news.IsRecent() { t.Error("Expected news post from 2 hours ago to be recent") } - + // Create recent post recentNews, _ := NewBuilder(db). WithAuthor(1). WithContent("Recent post"). Create() - + if !recentNews.IsRecent() { t.Error("Expected newly created post to be recent") } - + // Test Age age := news.Age() if age < 0 { t.Error("Expected positive age") } - + // Test IsAuthor if !news.IsAuthor(news.Author) { t.Error("Expected IsAuthor to return true for correct author") @@ -420,11 +420,11 @@ func TestUtilityMethods(t *testing.T) { if news.IsAuthor(999) { t.Error("Expected IsAuthor to return false for incorrect author") } - + // Test Preview longContent := "This is a very long content that should be truncated when preview is called" news.Content = longContent - + preview := news.Preview(20) if len(preview) > 20 { t.Errorf("Expected preview length <= 20, got %d", len(preview)) @@ -432,28 +432,28 @@ func TestUtilityMethods(t *testing.T) { if preview[len(preview)-3:] != "..." { t.Error("Expected preview to end with ellipsis") } - + shortPreview := news.Preview(100) // Longer than content if shortPreview != longContent { t.Error("Expected short content to not be truncated") } - + // Test WordCount news.Content = "This is a test with five words" wordCount := news.WordCount() if wordCount != 7 { t.Errorf("Expected 7 words, got %d", wordCount) } - + news.Content = "" emptyWordCount := news.WordCount() if emptyWordCount != 0 { t.Errorf("Expected 0 words for empty content, got %d", emptyWordCount) } - + news.Content = "OneWord" oneWordCount := news.WordCount() if oneWordCount != 1 { t.Errorf("Expected 1 word, got %d", oneWordCount) } -} \ No newline at end of file +} diff --git a/internal/template/doc.go b/internal/template/doc.go new file mode 100644 index 0000000..adee161 --- /dev/null +++ b/internal/template/doc.go @@ -0,0 +1,5 @@ +// Package template provides in-memory template caching with automatic reloading +// and placeholder replacement functionality. Templates are loaded from files +// adjacent to the binary and support both positional and named placeholder +// replacement with dot notation for accessing nested map values. +package template \ No newline at end of file diff --git a/internal/template/template.go b/internal/template/template.go new file mode 100644 index 0000000..da703b3 --- /dev/null +++ b/internal/template/template.go @@ -0,0 +1,226 @@ +package template + +import ( + "fmt" + "os" + "path/filepath" + "reflect" + "strings" + "sync" + "time" + + "github.com/valyala/fasthttp" +) + +type Cache struct { + mu sync.RWMutex + templates map[string]*Template + basePath string +} + +type Template struct { + name string + content string + modTime time.Time + filePath string +} + +func NewCache(basePath string) *Cache { + if basePath == "" { + exe, err := os.Executable() + if err != nil { + basePath = "." + } else { + basePath = filepath.Dir(exe) + } + } + + return &Cache{ + templates: make(map[string]*Template), + basePath: basePath, + } +} + +func (c *Cache) Load(name string) (*Template, error) { + c.mu.RLock() + tmpl, exists := c.templates[name] + c.mu.RUnlock() + + if exists { + if err := c.checkAndReload(tmpl); err != nil { + return nil, err + } + return tmpl, nil + } + + return c.loadFromFile(name) +} + +func (c *Cache) loadFromFile(name string) (*Template, error) { + filePath := filepath.Join(c.basePath, "templates", name) + + info, err := os.Stat(filePath) + if err != nil { + return nil, fmt.Errorf("template file not found: %s", name) + } + + content, err := os.ReadFile(filePath) + if err != nil { + return nil, fmt.Errorf("failed to read template: %w", err) + } + + tmpl := &Template{ + name: name, + content: string(content), + modTime: info.ModTime(), + filePath: filePath, + } + + c.mu.Lock() + c.templates[name] = tmpl + c.mu.Unlock() + + return tmpl, nil +} + +func (c *Cache) checkAndReload(tmpl *Template) error { + info, err := os.Stat(tmpl.filePath) + if err != nil { + return err + } + + if info.ModTime().After(tmpl.modTime) { + content, err := os.ReadFile(tmpl.filePath) + if err != nil { + return err + } + + c.mu.Lock() + tmpl.content = string(content) + tmpl.modTime = info.ModTime() + c.mu.Unlock() + } + + return nil +} + +func (t *Template) RenderPositional(args ...any) string { + result := t.content + for i, arg := range args { + placeholder := fmt.Sprintf("{%d}", i) + result = strings.ReplaceAll(result, placeholder, fmt.Sprintf("%v", arg)) + } + return result +} + +func (t *Template) RenderNamed(data map[string]any) string { + result := t.content + + for key, value := range data { + placeholder := fmt.Sprintf("{%s}", key) + result = strings.ReplaceAll(result, placeholder, fmt.Sprintf("%v", value)) + } + + result = t.replaceDotNotation(result, data) + + return result +} + +func (t *Template) replaceDotNotation(content string, data map[string]any) string { + result := content + + start := 0 + for { + startIdx := strings.Index(result[start:], "{") + if startIdx == -1 { + break + } + startIdx += start + + endIdx := strings.Index(result[startIdx:], "}") + if endIdx == -1 { + break + } + endIdx += startIdx + + placeholder := result[startIdx+1 : endIdx] + + if strings.Contains(placeholder, ".") { + value := t.getNestedValue(data, placeholder) + if value != nil { + result = result[:startIdx] + fmt.Sprintf("%v", value) + result[endIdx+1:] + start = startIdx + len(fmt.Sprintf("%v", value)) + continue + } + } + + start = endIdx + 1 + } + + return result +} + +func (t *Template) getNestedValue(data map[string]any, path string) any { + keys := strings.Split(path, ".") + current := data + + for i, key := range keys { + if i == len(keys)-1 { + return current[key] + } + + next, ok := current[key] + if !ok { + return nil + } + + switch v := next.(type) { + case map[string]any: + current = v + case map[any]any: + newMap := make(map[string]any) + for k, val := range v { + newMap[fmt.Sprintf("%v", k)] = val + } + current = newMap + default: + rv := reflect.ValueOf(next) + if rv.Kind() == reflect.Map { + newMap := make(map[string]any) + for _, k := range rv.MapKeys() { + newMap[fmt.Sprintf("%v", k.Interface())] = rv.MapIndex(k).Interface() + } + current = newMap + } else { + return nil + } + } + } + + return nil +} + +func (t *Template) WriteTo(ctx *fasthttp.RequestCtx, data any) { + var result string + + switch v := data.(type) { + case map[string]any: + result = t.RenderNamed(v) + case []any: + result = t.RenderPositional(v...) + default: + rv := reflect.ValueOf(data) + if rv.Kind() == reflect.Slice { + args := make([]any, rv.Len()) + for i := 0; i < rv.Len(); i++ { + args[i] = rv.Index(i).Interface() + } + result = t.RenderPositional(args...) + } else { + result = t.RenderPositional(data) + } + } + + ctx.SetContentType("text/html; charset=utf-8") + ctx.WriteString(result) +} diff --git a/internal/template/template_test.go b/internal/template/template_test.go new file mode 100644 index 0000000..5d057f3 --- /dev/null +++ b/internal/template/template_test.go @@ -0,0 +1,208 @@ +package template + +import ( + "os" + "path/filepath" + "testing" + "time" +) + +func TestNewCache(t *testing.T) { + cache := NewCache("") + if cache == nil { + t.Fatal("NewCache returned nil") + } + if cache.templates == nil { + t.Fatal("templates map not initialized") + } +} + +func TestPositionalReplacement(t *testing.T) { + tmpl := &Template{ + name: "test", + content: "Hello {0}, you are {1} years old!", + } + + result := tmpl.RenderPositional("Alice", 25) + expected := "Hello Alice, you are 25 years old!" + + if result != expected { + t.Errorf("Expected %q, got %q", expected, result) + } +} + +func TestNamedReplacement(t *testing.T) { + tmpl := &Template{ + name: "test", + content: "Hello {name}, you are {age} years old!", + } + + data := map[string]any{ + "name": "Bob", + "age": 30, + } + + result := tmpl.RenderNamed(data) + expected := "Hello Bob, you are 30 years old!" + + if result != expected { + t.Errorf("Expected %q, got %q", expected, result) + } +} + +func TestDotNotationReplacement(t *testing.T) { + tmpl := &Template{ + name: "test", + content: "User: {user.name}, Email: {user.contact.email}", + } + + data := map[string]any{ + "user": map[string]any{ + "name": "Charlie", + "contact": map[string]any{ + "email": "charlie@example.com", + }, + }, + } + + result := tmpl.RenderNamed(data) + expected := "User: Charlie, Email: charlie@example.com" + + if result != expected { + t.Errorf("Expected %q, got %q", expected, result) + } +} + +func TestTemplateLoadingAndCaching(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "template_test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + templatesDir := filepath.Join(tmpDir, "templates") + err = os.MkdirAll(templatesDir, 0755) + if err != nil { + t.Fatal(err) + } + + templateFile := filepath.Join(templatesDir, "test.html") + content := "Hello {name}!" + err = os.WriteFile(templateFile, []byte(content), 0644) + if err != nil { + t.Fatal(err) + } + + cache := NewCache(tmpDir) + + tmpl, err := cache.Load("test.html") + if err != nil { + t.Fatal(err) + } + + if tmpl.content != content { + t.Errorf("Expected content %q, got %q", content, tmpl.content) + } + + tmpl2, err := cache.Load("test.html") + if err != nil { + t.Fatal(err) + } + + if tmpl != tmpl2 { + t.Error("Template should be cached and return same instance") + } +} + +func TestTemplateReloading(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "template_test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + templatesDir := filepath.Join(tmpDir, "templates") + err = os.MkdirAll(templatesDir, 0755) + if err != nil { + t.Fatal(err) + } + + templateFile := filepath.Join(templatesDir, "test.html") + content1 := "Hello {name}!" + err = os.WriteFile(templateFile, []byte(content1), 0644) + if err != nil { + t.Fatal(err) + } + + cache := NewCache(tmpDir) + + tmpl, err := cache.Load("test.html") + if err != nil { + t.Fatal(err) + } + + if tmpl.content != content1 { + t.Errorf("Expected content %q, got %q", content1, tmpl.content) + } + + time.Sleep(10 * time.Millisecond) + + content2 := "Hi {name}, welcome!" + err = os.WriteFile(templateFile, []byte(content2), 0644) + if err != nil { + t.Fatal(err) + } + + tmpl2, err := cache.Load("test.html") + if err != nil { + t.Fatal(err) + } + + if tmpl2.content != content2 { + t.Errorf("Expected reloaded content %q, got %q", content2, tmpl2.content) + } +} + +func TestGetNestedValue(t *testing.T) { + tmpl := &Template{} + + data := map[string]any{ + "level1": map[string]any{ + "level2": map[string]any{ + "value": "found", + }, + }, + } + + result := tmpl.getNestedValue(data, "level1.level2.value") + if result != "found" { + t.Errorf("Expected 'found', got %v", result) + } + + result = tmpl.getNestedValue(data, "level1.nonexistent") + if result != nil { + t.Errorf("Expected nil for nonexistent path, got %v", result) + } +} + +func TestMixedReplacementTypes(t *testing.T) { + tmpl := &Template{ + name: "test", + content: "Hello {name}, you have {count} {items.type}s!", + } + + data := map[string]any{ + "name": "Dave", + "count": 5, + "items": map[string]any{ + "type": "apple", + }, + } + + result := tmpl.RenderNamed(data) + expected := "Hello Dave, you have 5 apples!" + + if result != expected { + t.Errorf("Expected %q, got %q", expected, result) + } +} diff --git a/internal/users/doc.go b/internal/users/doc.go index 5220e5e..9387430 100644 --- a/internal/users/doc.go +++ b/internal/users/doc.go @@ -71,12 +71,12 @@ The builder automatically sets sensible defaults for all fields if not specified ## Authentication and Verification user, _ := users.Find(db, userID) - + // Check verification status if user.IsVerified() { fmt.Println("User email is verified") } - + // Check authorization levels if user.IsAdmin() { fmt.Println("User has admin privileges") @@ -90,11 +90,11 @@ The builder automatically sets sensible defaults for all fields if not specified // Update last online time user.UpdateLastOnline() user.Save() - + // Get activity information registered := user.RegisteredTime() lastOnline := user.LastOnlineTime() - + fmt.Printf("Registered: %s\n", registered.Format("Jan 2, 2006")) fmt.Printf("Last online: %s\n", lastOnline.Format("Jan 2 15:04")) @@ -103,13 +103,13 @@ The builder automatically sets sensible defaults for all fields if not specified ## Stats and Progression user, _ := users.Find(db, userID) - + // Get character stats stats := user.GetStats() - fmt.Printf("Level %d: HP %d/%d, MP %d/%d\n", - stats["level"], stats["hp"], stats["max_hp"], + fmt.Printf("Level %d: HP %d/%d, MP %d/%d\n", + stats["level"], stats["hp"], stats["max_hp"], stats["mp"], stats["max_mp"]) - + // Update character progression user.Level = 10 user.Exp = 5000 @@ -122,7 +122,7 @@ The builder automatically sets sensible defaults for all fields if not specified // Get current position x, y := user.GetPosition() fmt.Printf("Player at (%d, %d)\n", x, y) - + // Move player user.SetPosition(newX, newY) user.Currently = "Exploring the forest" @@ -131,10 +131,10 @@ The builder automatically sets sensible defaults for all fields if not specified ## Combat Status if user.IsFighting() { - fmt.Printf("Fighting monster ID %d (HP: %d)\n", + fmt.Printf("Fighting monster ID %d (HP: %d)\n", user.MonsterID, user.MonsterHP) } - + if user.IsAlive() { fmt.Printf("Player has %d HP remaining\n", user.HP) } @@ -144,16 +144,16 @@ The builder automatically sets sensible defaults for all fields if not specified ## Spell Management user, _ := users.Find(db, userID) - + // Get known spells spells := user.GetSpellIDs() fmt.Printf("Player knows %d spells: %v\n", len(spells), spells) - + // Check if player knows a specific spell if user.HasSpell("5") { fmt.Println("Player knows spell 5") } - + // Learn new spells newSpells := append(spells, "7", "8") user.SetSpellIDs(newSpells) @@ -166,11 +166,11 @@ The builder automatically sets sensible defaults for all fields if not specified if err != nil { return err } - + if !user.HasSpell(spellID) { return fmt.Errorf("user doesn't know spell %s", spellID) } - + // Spell casting logic here... return nil } @@ -180,16 +180,16 @@ The builder automatically sets sensible defaults for all fields if not specified ## Town Visits user, _ := users.Find(db, userID) - + // Get visited towns towns := user.GetTownIDs() fmt.Printf("Visited %d towns: %v\n", len(towns), towns) - + // Check if player has visited a town if user.HasVisitedTown("3") { fmt.Println("Player has been to town 3") } - + // Visit new town visitedTowns := append(towns, "4") user.SetTownIDs(visitedTowns) @@ -202,13 +202,13 @@ The builder automatically sets sensible defaults for all fields if not specified if err != nil { return err } - + // Add town to visited list if not already there if !user.HasVisitedTown(townID) { towns := user.GetTownIDs() user.SetTownIDs(append(towns, townID)) } - + // Update position and status // town coordinates would be looked up here user.Currently = fmt.Sprintf("In town %s", townID) @@ -220,15 +220,15 @@ The builder automatically sets sensible defaults for all fields if not specified ## Equipment Management user, _ := users.Find(db, userID) - + // Get all equipment equipment := user.GetEquipment() - weapon := equipment["weapon"].(map[string]interface{}) - armor := equipment["armor"].(map[string]interface{}) - + weapon := equipment["weapon"].(map[string]any) + armor := equipment["armor"].(map[string]any) + fmt.Printf("Weapon: %s (ID: %d)\n", weapon["name"], weapon["id"]) fmt.Printf("Armor: %s (ID: %d)\n", armor["name"], armor["id"]) - + // Equip new items user.WeaponID = 15 user.WeaponName = "Dragon Sword" @@ -245,7 +245,7 @@ The builder automatically sets sensible defaults for all fields if not specified if err != nil { log.Fatal(err) } - + fmt.Printf("Level 5 players (%d):\n", len(level5Players)) for _, player := range level5Players { fmt.Printf("- %s (EXP: %d)\n", player.Username, player.Exp) @@ -258,7 +258,7 @@ The builder automatically sets sensible defaults for all fields if not specified if err != nil { log.Fatal(err) } - + fmt.Printf("Players online in last hour (%d):\n", len(onlinePlayers)) for _, player := range onlinePlayers { lastSeen := time.Since(player.LastOnlineTime()) @@ -332,18 +332,18 @@ The users table contains extensive character and game state information: func levelUpCharacter(user *users.User, newLevel int) { user.Level = newLevel - + // Increase base stats user.MaxHP += 5 user.HP = user.MaxHP // Full heal on level up user.MaxMP += 2 user.MP = user.MaxMP - + // Stat bonuses user.Strength++ user.Attack++ user.Defense++ - + user.Save() } @@ -353,23 +353,23 @@ The users table contains extensive character and game state information: if user.IsFighting() { return fmt.Errorf("already in combat") } - + user.Fighting = 1 user.MonsterID = monsterID // monster HP would be looked up from monsters table user.MonsterHP = 50 user.Currently = "Fighting" - + return user.Save() } - + func endCombat(user *users.User, won bool) error { user.Fighting = 0 user.MonsterID = 0 user.MonsterHP = 0 user.MonsterSleep = 0 user.MonsterImmune = 0 - + if won { user.Currently = "Victorious" // Award experience and gold @@ -377,7 +377,7 @@ The users table contains extensive character and game state information: user.Currently = "Defeated" user.HP = 0 // Player defeated } - + return user.Save() } @@ -388,24 +388,24 @@ The users table contains extensive character and game state information: if err != nil { return err } - + user.Auth = authLevel return user.Save() } - + func getUsersByAuthLevel(db *database.DB, minAuth int) ([]*users.User, error) { allUsers, err := users.All(db) if err != nil { return nil, err } - + var authorizedUsers []*users.User for _, user := range allUsers { if user.Auth >= minAuth { authorizedUsers = append(authorizedUsers, user) } } - + return authorizedUsers, nil } @@ -418,7 +418,7 @@ The users table is large and frequently accessed. Consider: // Use specific lookups when possible user, _ := users.ByUsername(db, username) // Uses index user, _ := users.ByEmail(db, email) // Uses index - + // Limit results for admin interfaces onlineUsers, _ := users.Online(db, time.Hour) // Bounded by time levelUsers, _ := users.ByLevel(db, targetLevel) // Bounded by level @@ -430,7 +430,7 @@ The users table is large and frequently accessed. Consider: users map[int]*users.User mutex sync.RWMutex } - + func (c *UserCache) GetUser(db *database.DB, id int) (*users.User, error) { c.mutex.RLock() if user, ok := c.users[id]; ok { @@ -438,16 +438,16 @@ The users table is large and frequently accessed. Consider: return user, nil } c.mutex.RUnlock() - + user, err := users.Find(db, id) if err != nil { return nil, err } - + c.mutex.Lock() c.users[id] = user c.mutex.Unlock() - + return user, nil } @@ -460,20 +460,20 @@ The users table is large and frequently accessed. Consider: if err != nil { return nil, fmt.Errorf("user not found") } - + if !user.IsVerified() { return nil, fmt.Errorf("email not verified") } - + // Verify password (implement password checking) if !verifyPassword(user.Password, password) { return nil, fmt.Errorf("invalid password") } - + // Update last online user.UpdateLastOnline() user.Save() - + return user, nil } @@ -485,16 +485,16 @@ The users table is large and frequently accessed. Consider: user.HP = gameData.HP user.MP = gameData.MP user.Currently = gameData.Status - + if gameData.InCombat { user.Fighting = 1 user.MonsterID = gameData.MonsterID user.MonsterHP = gameData.MonsterHP } - + return user.Save() } The users package provides comprehensive player account management with support for all game mechanics including character progression, combat, equipment, spells, and world exploration. */ -package users \ No newline at end of file +package users diff --git a/internal/users/users.go b/internal/users/users.go index 92740c0..8c14579 100644 --- a/internal/users/users.go +++ b/internal/users/users.go @@ -263,12 +263,12 @@ func (u *User) Save() error { return fmt.Errorf("cannot save user without ID") } - query := `UPDATE users SET username = ?, password = ?, email = ?, verified = ?, token = ?, + query := `UPDATE users SET username = ?, password = ?, email = ?, verified = ?, token = ?, registered = ?, last_online = ?, auth = ?, x = ?, y = ?, class_id = ?, currently = ?, fighting = ?, monster_id = ?, monster_hp = ?, monster_sleep = ?, monster_immune = ?, uber_damage = ?, uber_defense = ?, hp = ?, mp = ?, tp = ?, max_hp = ?, max_mp = ?, max_tp = ?, level = ?, gold = ?, exp = ?, gold_bonus = ?, exp_bonus = ?, strength = ?, dexterity = ?, - attack = ?, defense = ?, weapon_id = ?, armor_id = ?, shield_id = ?, slot_1_id = ?, + attack = ?, defense = ?, weapon_id = ?, armor_id = ?, shield_id = ?, slot_1_id = ?, slot_2_id = ?, slot_3_id = ?, weapon_name = ?, armor_name = ?, shield_name = ?, slot_1_name = ?, slot_2_name = ?, slot_3_name = ?, drop_code = ?, spells = ?, towns = ? WHERE id = ?` @@ -382,32 +382,32 @@ func (u *User) HasVisitedTown(townID string) bool { } // GetEquipment returns all equipped item information -func (u *User) GetEquipment() map[string]interface{} { - return map[string]interface{}{ - "weapon": map[string]interface{}{"id": u.WeaponID, "name": u.WeaponName}, - "armor": map[string]interface{}{"id": u.ArmorID, "name": u.ArmorName}, - "shield": map[string]interface{}{"id": u.ShieldID, "name": u.ShieldName}, - "slot1": map[string]interface{}{"id": u.Slot1ID, "name": u.Slot1Name}, - "slot2": map[string]interface{}{"id": u.Slot2ID, "name": u.Slot2Name}, - "slot3": map[string]interface{}{"id": u.Slot3ID, "name": u.Slot3Name}, +func (u *User) GetEquipment() map[string]any { + return map[string]any{ + "weapon": map[string]any{"id": u.WeaponID, "name": u.WeaponName}, + "armor": map[string]any{"id": u.ArmorID, "name": u.ArmorName}, + "shield": map[string]any{"id": u.ShieldID, "name": u.ShieldName}, + "slot1": map[string]any{"id": u.Slot1ID, "name": u.Slot1Name}, + "slot2": map[string]any{"id": u.Slot2ID, "name": u.Slot2Name}, + "slot3": map[string]any{"id": u.Slot3ID, "name": u.Slot3Name}, } } // GetStats returns combat-relevant stats func (u *User) GetStats() map[string]int { return map[string]int{ - "level": u.Level, - "hp": u.HP, - "mp": u.MP, - "tp": u.TP, - "max_hp": u.MaxHP, - "max_mp": u.MaxMP, - "max_tp": u.MaxTP, - "strength": u.Strength, - "dexterity": u.Dexterity, - "attack": u.Attack, - "defense": u.Defense, - "uber_damage": u.UberDamage, + "level": u.Level, + "hp": u.HP, + "mp": u.MP, + "tp": u.TP, + "max_hp": u.MaxHP, + "max_mp": u.MaxMP, + "max_tp": u.MaxTP, + "strength": u.Strength, + "dexterity": u.Dexterity, + "attack": u.Attack, + "defense": u.Defense, + "uber_damage": u.UberDamage, "uber_defense": u.UberDefense, } } @@ -421,4 +421,4 @@ func (u *User) GetPosition() (int, int) { func (u *User) SetPosition(x, y int) { u.X = x u.Y = y -} \ No newline at end of file +} diff --git a/internal/users/users_test.go b/internal/users/users_test.go index 4b8e096..00b52bc 100644 --- a/internal/users/users_test.go +++ b/internal/users/users_test.go @@ -79,7 +79,7 @@ func setupTestDB(t *testing.T) *database.DB { // Insert test data with specific timestamps now := time.Now().Unix() - testUsers := `INSERT INTO users (username, password, email, verified, token, registered, last_online, auth, + testUsers := `INSERT INTO users (username, password, email, verified, token, registered, last_online, auth, x, y, class_id, level, gold, exp, hp, mp, tp, max_hp, max_mp, max_tp, strength, dexterity, attack, defense, spells, towns) VALUES ('alice', 'hashed_pass_1', 'alice@example.com', 1, '', ?, ?, 0, 10, 20, 1, 5, 500, 1250, 25, 15, 12, 25, 15, 12, 8, 7, 10, 8, '1,2,5', '1,2'), @@ -87,11 +87,11 @@ func setupTestDB(t *testing.T) *database.DB { ('charlie', 'hashed_pass_3', 'charlie@example.com', 0, 'verify_token_123', ?, ?, 4, 0, 0, 3, 1, 100, 0, 15, 0, 10, 15, 0, 10, 5, 5, 5, 5, '', ''), ('diana', 'hashed_pass_4', 'diana@example.com', 1, '', ?, ?, 0, 25, -10, 1, 8, 1200, 3500, 35, 25, 15, 35, 25, 15, 12, 10, 15, 12, '1,2,3,6,7', '1,2,3,4')` - timestamps := []interface{}{ - now - 86400*7, now - 3600*2, // alice: registered 1 week ago, last online 2 hours ago - now - 86400*5, now - 86400*1, // bob: registered 5 days ago, last online 1 day ago - now - 86400*1, now - 86400*1, // charlie: registered 1 day ago, last online 1 day ago - now - 86400*30, now - 3600*1, // diana: registered 1 month ago, last online 1 hour ago + timestamps := []any{ + now - 86400*7, now - 3600*2, // alice: registered 1 week ago, last online 2 hours ago + now - 86400*5, now - 86400*1, // bob: registered 5 days ago, last online 1 day ago + now - 86400*1, now - 86400*1, // charlie: registered 1 day ago, last online 1 day ago + now - 86400*30, now - 3600*1, // diana: registered 1 month ago, last online 1 hour ago } if err := db.Exec(testUsers, timestamps...); err != nil { @@ -477,9 +477,9 @@ func TestUserMethods(t *testing.T) { db := setupTestDB(t) defer db.Close() - alice, _ := Find(db, 1) // verified, auth 0 - bob, _ := Find(db, 2) // verified, auth 2 (moderator) - charlie, _ := Find(db, 3) // unverified, auth 4 (admin) + alice, _ := Find(db, 1) // verified, auth 0 + bob, _ := Find(db, 2) // verified, auth 2 (moderator) + charlie, _ := Find(db, 3) // unverified, auth 4 (admin) // Test time methods registeredTime := alice.RegisteredTime() @@ -644,7 +644,7 @@ func TestGetEquipmentAndStats(t *testing.T) { t.Error("Expected non-nil equipment map") } - weapon, ok := equipment["weapon"].(map[string]interface{}) + weapon, ok := equipment["weapon"].(map[string]any) if !ok { t.Error("Expected weapon to be a map") } @@ -667,4 +667,4 @@ func TestGetEquipmentAndStats(t *testing.T) { if stats["strength"] != alice.Strength { t.Errorf("Expected strength %d, got %d", alice.Strength, stats["strength"]) } -} \ No newline at end of file +}