create template package
This commit is contained in:
parent
c7d08d8004
commit
2efa1e0d07
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
5
internal/template/doc.go
Normal file
5
internal/template/doc.go
Normal file
@ -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
|
226
internal/template/template.go
Normal file
226
internal/template/template.go
Normal file
@ -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)
|
||||
}
|
208
internal/template/template_test.go
Normal file
208
internal/template/template_test.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
package users
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user