Compare commits

...

2 Commits

Author SHA1 Message Date
0c9a701cef enhance template engine with inheritence, composition 2025-08-09 08:48:24 -05:00
2efa1e0d07 create template package 2025-08-09 08:28:21 -05:00
11 changed files with 1110 additions and 269 deletions

View File

@ -13,12 +13,12 @@ func setupTestDB(t *testing.T) *database.DB {
t.Cleanup(func() { t.Cleanup(func() {
os.Remove(testDB) os.Remove(testDB)
}) })
db, err := database.Open(testDB) db, err := database.Open(testDB)
if err != nil { if err != nil {
t.Fatalf("Failed to open test database: %v", err) t.Fatalf("Failed to open test database: %v", err)
} }
// Create babble table // Create babble table
createTable := `CREATE TABLE babble ( createTable := `CREATE TABLE babble (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -26,11 +26,11 @@ func setupTestDB(t *testing.T) *database.DB {
author TEXT NOT NULL DEFAULT '', author TEXT NOT NULL DEFAULT '',
babble TEXT NOT NULL DEFAULT '' babble TEXT NOT NULL DEFAULT ''
)` )`
if err := db.Exec(createTable); err != nil { if err := db.Exec(createTable); err != nil {
t.Fatalf("Failed to create babble table: %v", err) t.Fatalf("Failed to create babble table: %v", err)
} }
// Insert test data with specific timestamps for predictable testing // Insert test data with specific timestamps for predictable testing
now := time.Now().Unix() now := time.Now().Unix()
testBabble := `INSERT INTO babble (posted, author, babble) VALUES 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'), (?, 'Alice', 'I can help @Charlie, let me know'),
(?, 'David', 'Server lag is really bad right now...'), (?, 'David', 'Server lag is really bad right now...'),
(?, 'Eve', 'Quick question about spell mechanics')` (?, 'Eve', 'Quick question about spell mechanics')`
timestamps := []interface{}{ timestamps := []any{
now - 3600*6, // 6 hours ago 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*2, // 2 hours ago
now - 3600*1, // 1 hour ago now - 3600*1, // 1 hour ago
now - 1800, // 30 minutes ago now - 1800, // 30 minutes ago
now - 300, // 5 minutes ago now - 300, // 5 minutes ago
} }
if err := db.Exec(testBabble, timestamps...); err != nil { if err := db.Exec(testBabble, timestamps...); err != nil {
t.Fatalf("Failed to insert test babble: %v", err) t.Fatalf("Failed to insert test babble: %v", err)
} }
return db return db
} }
func TestFind(t *testing.T) { func TestFind(t *testing.T) {
db := setupTestDB(t) db := setupTestDB(t)
defer db.Close() defer db.Close()
// Test finding existing babble // Test finding existing babble
babble, err := Find(db, 1) babble, err := Find(db, 1)
if err != nil { if err != nil {
t.Fatalf("Failed to find babble: %v", err) t.Fatalf("Failed to find babble: %v", err)
} }
if babble.ID != 1 { if babble.ID != 1 {
t.Errorf("Expected ID 1, got %d", babble.ID) t.Errorf("Expected ID 1, got %d", babble.ID)
} }
@ -79,7 +79,7 @@ func TestFind(t *testing.T) {
if babble.Posted == 0 { if babble.Posted == 0 {
t.Error("Expected non-zero posted timestamp") t.Error("Expected non-zero posted timestamp")
} }
// Test finding non-existent babble // Test finding non-existent babble
_, err = Find(db, 999) _, err = Find(db, 999)
if err == nil { if err == nil {
@ -90,23 +90,23 @@ func TestFind(t *testing.T) {
func TestAll(t *testing.T) { func TestAll(t *testing.T) {
db := setupTestDB(t) db := setupTestDB(t)
defer db.Close() defer db.Close()
babbles, err := All(db) babbles, err := All(db)
if err != nil { if err != nil {
t.Fatalf("Failed to get all babble: %v", err) t.Fatalf("Failed to get all babble: %v", err)
} }
if len(babbles) != 6 { if len(babbles) != 6 {
t.Errorf("Expected 6 babble messages, got %d", len(babbles)) t.Errorf("Expected 6 babble messages, got %d", len(babbles))
} }
// Check ordering (newest first) // Check ordering (newest first)
if len(babbles) >= 2 { if len(babbles) >= 2 {
if babbles[0].Posted < babbles[1].Posted { if babbles[0].Posted < babbles[1].Posted {
t.Error("Expected babble to be ordered by posted time (newest first)") t.Error("Expected babble to be ordered by posted time (newest first)")
} }
} }
// First message should be the most recent (5 minutes ago) // First message should be the most recent (5 minutes ago)
if babbles[0].Author != "Eve" { if babbles[0].Author != "Eve" {
t.Errorf("Expected newest message from Eve, got from '%s'", babbles[0].Author) 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) { func TestByAuthor(t *testing.T) {
db := setupTestDB(t) db := setupTestDB(t)
defer db.Close() defer db.Close()
// Test messages by Alice // Test messages by Alice
aliceMessages, err := ByAuthor(db, "Alice") aliceMessages, err := ByAuthor(db, "Alice")
if err != nil { if err != nil {
t.Fatalf("Failed to get babble by author: %v", err) t.Fatalf("Failed to get babble by author: %v", err)
} }
if len(aliceMessages) != 2 { if len(aliceMessages) != 2 {
t.Errorf("Expected 2 messages by Alice, got %d", len(aliceMessages)) t.Errorf("Expected 2 messages by Alice, got %d", len(aliceMessages))
} }
// Verify all messages are by Alice // Verify all messages are by Alice
for _, message := range aliceMessages { for _, message := range aliceMessages {
if message.Author != "Alice" { if message.Author != "Alice" {
t.Errorf("Expected author 'Alice', got '%s'", message.Author) t.Errorf("Expected author 'Alice', got '%s'", message.Author)
} }
} }
// Check ordering (newest first) // Check ordering (newest first)
if len(aliceMessages) == 2 { if len(aliceMessages) == 2 {
if aliceMessages[0].Babble != "I can help @Charlie, let me know" { if aliceMessages[0].Babble != "I can help @Charlie, let me know" {
t.Errorf("Expected newest message by Alice first") t.Errorf("Expected newest message by Alice first")
} }
} }
// Test case insensitive search // Test case insensitive search
aliceMessagesLower, err := ByAuthor(db, "alice") aliceMessagesLower, err := ByAuthor(db, "alice")
if err != nil { if err != nil {
t.Fatalf("Failed to get babble by lowercase author: %v", err) t.Fatalf("Failed to get babble by lowercase author: %v", err)
} }
if len(aliceMessagesLower) != 2 { if len(aliceMessagesLower) != 2 {
t.Errorf("Expected case insensitive search to find 2 messages, got %d", len(aliceMessagesLower)) t.Errorf("Expected case insensitive search to find 2 messages, got %d", len(aliceMessagesLower))
} }
// Test author with no messages // Test author with no messages
noMessages, err := ByAuthor(db, "NonexistentUser") noMessages, err := ByAuthor(db, "NonexistentUser")
if err != nil { if err != nil {
t.Fatalf("Failed to query non-existent author: %v", err) t.Fatalf("Failed to query non-existent author: %v", err)
} }
if len(noMessages) != 0 { if len(noMessages) != 0 {
t.Errorf("Expected 0 messages by non-existent author, got %d", len(noMessages)) 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) { func TestRecent(t *testing.T) {
db := setupTestDB(t) db := setupTestDB(t)
defer db.Close() defer db.Close()
// Test getting 3 most recent messages // Test getting 3 most recent messages
recentMessages, err := Recent(db, 3) recentMessages, err := Recent(db, 3)
if err != nil { if err != nil {
t.Fatalf("Failed to get recent babble: %v", err) t.Fatalf("Failed to get recent babble: %v", err)
} }
if len(recentMessages) != 3 { if len(recentMessages) != 3 {
t.Errorf("Expected 3 recent messages, got %d", len(recentMessages)) t.Errorf("Expected 3 recent messages, got %d", len(recentMessages))
} }
// Check ordering (newest first) // Check ordering (newest first)
if len(recentMessages) >= 2 { if len(recentMessages) >= 2 {
if recentMessages[0].Posted < recentMessages[1].Posted { if recentMessages[0].Posted < recentMessages[1].Posted {
t.Error("Expected recent messages to be ordered newest first") t.Error("Expected recent messages to be ordered newest first")
} }
} }
// Test getting more messages than exist // Test getting more messages than exist
allRecentMessages, err := Recent(db, 10) allRecentMessages, err := Recent(db, 10)
if err != nil { if err != nil {
t.Fatalf("Failed to get recent babble with high limit: %v", err) t.Fatalf("Failed to get recent babble with high limit: %v", err)
} }
if len(allRecentMessages) != 6 { if len(allRecentMessages) != 6 {
t.Errorf("Expected 6 messages (all available), got %d", len(allRecentMessages)) 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) { func TestSince(t *testing.T) {
db := setupTestDB(t) db := setupTestDB(t)
defer db.Close() defer db.Close()
// Test messages since 3 hours ago // Test messages since 3 hours ago
threeHoursAgo := time.Now().Add(-3 * time.Hour).Unix() threeHoursAgo := time.Now().Add(-3 * time.Hour).Unix()
recentMessages, err := Since(db, threeHoursAgo) recentMessages, err := Since(db, threeHoursAgo)
if err != nil { if err != nil {
t.Fatalf("Failed to get babble since timestamp: %v", err) 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 // Should get messages from 2 hours ago, 1 hour ago, 30 minutes ago, and 5 minutes ago
expectedCount := 4 expectedCount := 4
if len(recentMessages) != expectedCount { if len(recentMessages) != expectedCount {
t.Errorf("Expected %d messages since 3 hours ago, got %d", expectedCount, len(recentMessages)) t.Errorf("Expected %d messages since 3 hours ago, got %d", expectedCount, len(recentMessages))
} }
// Verify all messages are since the timestamp // Verify all messages are since the timestamp
for _, message := range recentMessages { for _, message := range recentMessages {
if message.Posted < threeHoursAgo { if message.Posted < threeHoursAgo {
t.Errorf("Message with timestamp %d is before the 'since' timestamp %d", 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) // Test with future timestamp (should return no messages)
futureMessages, err := Since(db, time.Now().Add(time.Hour).Unix()) futureMessages, err := Since(db, time.Now().Add(time.Hour).Unix())
if err != nil { if err != nil {
t.Fatalf("Failed to query future timestamp: %v", err) t.Fatalf("Failed to query future timestamp: %v", err)
} }
if len(futureMessages) != 0 { if len(futureMessages) != 0 {
t.Errorf("Expected 0 messages since future timestamp, got %d", len(futureMessages)) 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) { func TestBetween(t *testing.T) {
db := setupTestDB(t) db := setupTestDB(t)
defer db.Close() defer db.Close()
// Test messages between 5 hours ago and 1 hour ago // Test messages between 5 hours ago and 1 hour ago
start := time.Now().Add(-5 * time.Hour).Unix() start := time.Now().Add(-5 * time.Hour).Unix()
end := time.Now().Add(-1 * time.Hour).Unix() end := time.Now().Add(-1 * time.Hour).Unix()
betweenMessages, err := Between(db, start, end) betweenMessages, err := Between(db, start, end)
if err != nil { if err != nil {
t.Fatalf("Failed to get babble between timestamps: %v", err) 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) // Should get messages from 4 hours ago, 2 hours ago, and 1 hour ago (inclusive end)
expectedCount := 3 expectedCount := 3
if len(betweenMessages) != expectedCount { if len(betweenMessages) != expectedCount {
t.Errorf("Expected %d messages between timestamps, got %d", expectedCount, len(betweenMessages)) t.Errorf("Expected %d messages between timestamps, got %d", expectedCount, len(betweenMessages))
} }
// Verify all messages are within the range // Verify all messages are within the range
for _, message := range betweenMessages { for _, message := range betweenMessages {
if message.Posted < start || message.Posted > end { if message.Posted < start || message.Posted > end {
@ -259,41 +259,41 @@ func TestBetween(t *testing.T) {
func TestSearch(t *testing.T) { func TestSearch(t *testing.T) {
db := setupTestDB(t) db := setupTestDB(t)
defer db.Close() defer db.Close()
// Test searching for "game" // Test searching for "game"
gameMessages, err := Search(db, "game") gameMessages, err := Search(db, "game")
if err != nil { if err != nil {
t.Fatalf("Failed to search babble: %v", err) t.Fatalf("Failed to search babble: %v", err)
} }
expectedCount := 2 // Alice's welcome message and Bob's response expectedCount := 2 // Alice's welcome message and Bob's response
if len(gameMessages) != expectedCount { if len(gameMessages) != expectedCount {
t.Errorf("Expected %d messages containing 'game', got %d", expectedCount, len(gameMessages)) t.Errorf("Expected %d messages containing 'game', got %d", expectedCount, len(gameMessages))
} }
// Verify all messages contain the search term // Verify all messages contain the search term
for _, message := range gameMessages { for _, message := range gameMessages {
if !message.Contains("game") { if !message.Contains("game") {
t.Errorf("Message '%s' does not contain search term 'game'", message.Babble) t.Errorf("Message '%s' does not contain search term 'game'", message.Babble)
} }
} }
// Test case insensitive search // Test case insensitive search
gameMessagesUpper, err := Search(db, "GAME") gameMessagesUpper, err := Search(db, "GAME")
if err != nil { if err != nil {
t.Fatalf("Failed to search babble with uppercase: %v", err) t.Fatalf("Failed to search babble with uppercase: %v", err)
} }
if len(gameMessagesUpper) != expectedCount { if len(gameMessagesUpper) != expectedCount {
t.Error("Expected case insensitive search to find same results") t.Error("Expected case insensitive search to find same results")
} }
// Test search with no results // Test search with no results
noResults, err := Search(db, "nonexistentterm") noResults, err := Search(db, "nonexistentterm")
if err != nil { if err != nil {
t.Fatalf("Failed to search for non-existent term: %v", err) t.Fatalf("Failed to search for non-existent term: %v", err)
} }
if len(noResults) != 0 { if len(noResults) != 0 {
t.Errorf("Expected 0 results for non-existent term, got %d", len(noResults)) 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) { func TestRecentByAuthor(t *testing.T) {
db := setupTestDB(t) db := setupTestDB(t)
defer db.Close() defer db.Close()
// Test recent messages by Alice (limit 1) // Test recent messages by Alice (limit 1)
aliceRecent, err := RecentByAuthor(db, "Alice", 1) aliceRecent, err := RecentByAuthor(db, "Alice", 1)
if err != nil { if err != nil {
t.Fatalf("Failed to get recent babble by author: %v", err) t.Fatalf("Failed to get recent babble by author: %v", err)
} }
if len(aliceRecent) != 1 { if len(aliceRecent) != 1 {
t.Errorf("Expected 1 recent message by Alice, got %d", len(aliceRecent)) 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" { if len(aliceRecent) > 0 && aliceRecent[0].Babble != "I can help @Charlie, let me know" {
t.Error("Expected most recent message by Alice") t.Error("Expected most recent message by Alice")
} }
// Test with higher limit // Test with higher limit
aliceAll, err := RecentByAuthor(db, "Alice", 5) aliceAll, err := RecentByAuthor(db, "Alice", 5)
if err != nil { if err != nil {
t.Fatalf("Failed to get all recent messages by Alice: %v", err) t.Fatalf("Failed to get all recent messages by Alice: %v", err)
} }
if len(aliceAll) != 2 { if len(aliceAll) != 2 {
t.Errorf("Expected 2 total messages by Alice, got %d", len(aliceAll)) 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) { func TestBuilder(t *testing.T) {
db := setupTestDB(t) db := setupTestDB(t)
defer db.Close() defer db.Close()
// Create new babble using builder // Create new babble using builder
testTime := time.Now() testTime := time.Now()
babble, err := NewBuilder(db). babble, err := NewBuilder(db).
@ -339,11 +339,11 @@ func TestBuilder(t *testing.T) {
WithBabble("Test message from builder"). WithBabble("Test message from builder").
WithPostedTime(testTime). WithPostedTime(testTime).
Create() Create()
if err != nil { if err != nil {
t.Fatalf("Failed to create babble with builder: %v", err) t.Fatalf("Failed to create babble with builder: %v", err)
} }
if babble.ID == 0 { if babble.ID == 0 {
t.Error("Expected non-zero ID after creation") t.Error("Expected non-zero ID after creation")
} }
@ -356,41 +356,41 @@ func TestBuilder(t *testing.T) {
if babble.Posted != testTime.Unix() { if babble.Posted != testTime.Unix() {
t.Errorf("Expected posted time %d, got %d", testTime.Unix(), babble.Posted) t.Errorf("Expected posted time %d, got %d", testTime.Unix(), babble.Posted)
} }
// Test WithMessage alias // Test WithMessage alias
babble2, err := NewBuilder(db). babble2, err := NewBuilder(db).
WithAuthor("TestUser2"). WithAuthor("TestUser2").
WithMessage("Using WithMessage alias"). WithMessage("Using WithMessage alias").
Create() Create()
if err != nil { if err != nil {
t.Fatalf("Failed to create babble with WithMessage: %v", err) t.Fatalf("Failed to create babble with WithMessage: %v", err)
} }
if babble2.Babble != "Using WithMessage alias" { if babble2.Babble != "Using WithMessage alias" {
t.Errorf("WithMessage alias failed, got '%s'", babble2.Babble) t.Errorf("WithMessage alias failed, got '%s'", babble2.Babble)
} }
// Verify it was saved to database // Verify it was saved to database
foundBabble, err := Find(db, babble.ID) foundBabble, err := Find(db, babble.ID)
if err != nil { if err != nil {
t.Fatalf("Failed to find created babble: %v", err) t.Fatalf("Failed to find created babble: %v", err)
} }
if foundBabble.Babble != "Test message from builder" { if foundBabble.Babble != "Test message from builder" {
t.Errorf("Created babble not found in database") t.Errorf("Created babble not found in database")
} }
// Test builder with default timestamp // Test builder with default timestamp
defaultBabble, err := NewBuilder(db). defaultBabble, err := NewBuilder(db).
WithAuthor("DefaultUser"). WithAuthor("DefaultUser").
WithBabble("Message with default timestamp"). WithBabble("Message with default timestamp").
Create() Create()
if err != nil { if err != nil {
t.Fatalf("Failed to create babble with default timestamp: %v", err) t.Fatalf("Failed to create babble with default timestamp: %v", err)
} }
// Should have recent timestamp (within last minute) // Should have recent timestamp (within last minute)
if time.Since(defaultBabble.PostedTime()) > time.Minute { if time.Since(defaultBabble.PostedTime()) > time.Minute {
t.Error("Expected default timestamp to be recent") t.Error("Expected default timestamp to be recent")
@ -400,29 +400,29 @@ func TestBuilder(t *testing.T) {
func TestSave(t *testing.T) { func TestSave(t *testing.T) {
db := setupTestDB(t) db := setupTestDB(t)
defer db.Close() defer db.Close()
babble, err := Find(db, 1) babble, err := Find(db, 1)
if err != nil { if err != nil {
t.Fatalf("Failed to find babble: %v", err) t.Fatalf("Failed to find babble: %v", err)
} }
// Modify babble // Modify babble
babble.Author = "UpdatedAuthor" babble.Author = "UpdatedAuthor"
babble.Babble = "Updated message content" babble.Babble = "Updated message content"
babble.Posted = time.Now().Unix() babble.Posted = time.Now().Unix()
// Save changes // Save changes
err = babble.Save() err = babble.Save()
if err != nil { if err != nil {
t.Fatalf("Failed to save babble: %v", err) t.Fatalf("Failed to save babble: %v", err)
} }
// Verify changes were saved // Verify changes were saved
updatedBabble, err := Find(db, 1) updatedBabble, err := Find(db, 1)
if err != nil { if err != nil {
t.Fatalf("Failed to find updated babble: %v", err) t.Fatalf("Failed to find updated babble: %v", err)
} }
if updatedBabble.Author != "UpdatedAuthor" { if updatedBabble.Author != "UpdatedAuthor" {
t.Errorf("Expected updated author 'UpdatedAuthor', got '%s'", updatedBabble.Author) t.Errorf("Expected updated author 'UpdatedAuthor', got '%s'", updatedBabble.Author)
} }
@ -434,18 +434,18 @@ func TestSave(t *testing.T) {
func TestDelete(t *testing.T) { func TestDelete(t *testing.T) {
db := setupTestDB(t) db := setupTestDB(t)
defer db.Close() defer db.Close()
babble, err := Find(db, 1) babble, err := Find(db, 1)
if err != nil { if err != nil {
t.Fatalf("Failed to find babble: %v", err) t.Fatalf("Failed to find babble: %v", err)
} }
// Delete babble // Delete babble
err = babble.Delete() err = babble.Delete()
if err != nil { if err != nil {
t.Fatalf("Failed to delete babble: %v", err) t.Fatalf("Failed to delete babble: %v", err)
} }
// Verify babble was deleted // Verify babble was deleted
_, err = Find(db, 1) _, err = Find(db, 1)
if err == nil { if err == nil {
@ -456,33 +456,33 @@ func TestDelete(t *testing.T) {
func TestUtilityMethods(t *testing.T) { func TestUtilityMethods(t *testing.T) {
db := setupTestDB(t) db := setupTestDB(t)
defer db.Close() defer db.Close()
babble, _ := Find(db, 1) babble, _ := Find(db, 1)
// Test PostedTime // Test PostedTime
postedTime := babble.PostedTime() postedTime := babble.PostedTime()
if postedTime.IsZero() { if postedTime.IsZero() {
t.Error("Expected non-zero posted time") t.Error("Expected non-zero posted time")
} }
// Test SetPostedTime // Test SetPostedTime
newTime := time.Now().Add(-30 * time.Minute) newTime := time.Now().Add(-30 * time.Minute)
babble.SetPostedTime(newTime) babble.SetPostedTime(newTime)
if babble.Posted != newTime.Unix() { if babble.Posted != newTime.Unix() {
t.Errorf("Expected posted timestamp %d, got %d", newTime.Unix(), babble.Posted) t.Errorf("Expected posted timestamp %d, got %d", newTime.Unix(), babble.Posted)
} }
// Test IsRecent (should be true for 30 minutes ago) // Test IsRecent (should be true for 30 minutes ago)
if !babble.IsRecent() { if !babble.IsRecent() {
t.Error("Expected message from 30 minutes ago to be recent") t.Error("Expected message from 30 minutes ago to be recent")
} }
// Test Age // Test Age
age := babble.Age() age := babble.Age()
if age < 0 { if age < 0 {
t.Error("Expected positive age") t.Error("Expected positive age")
} }
// Test IsAuthor // Test IsAuthor
if !babble.IsAuthor("Alice") { if !babble.IsAuthor("Alice") {
t.Error("Expected IsAuthor to return true for correct author") t.Error("Expected IsAuthor to return true for correct author")
@ -493,11 +493,11 @@ func TestUtilityMethods(t *testing.T) {
if babble.IsAuthor("Bob") { if babble.IsAuthor("Bob") {
t.Error("Expected IsAuthor to return false for incorrect author") t.Error("Expected IsAuthor to return false for incorrect author")
} }
// Test Preview // Test Preview
longMessage := "This is a very long chat message that should be truncated when preview is called for display purposes" longMessage := "This is a very long chat message that should be truncated when preview is called for display purposes"
babble.Babble = longMessage babble.Babble = longMessage
preview := babble.Preview(20) preview := babble.Preview(20)
if len(preview) > 20 { if len(preview) > 20 {
t.Errorf("Expected preview length <= 20, got %d", len(preview)) t.Errorf("Expected preview length <= 20, got %d", len(preview))
@ -505,25 +505,25 @@ func TestUtilityMethods(t *testing.T) {
if preview[len(preview)-3:] != "..." { if preview[len(preview)-3:] != "..." {
t.Error("Expected preview to end with ellipsis") t.Error("Expected preview to end with ellipsis")
} }
shortPreview := babble.Preview(200) // Longer than message shortPreview := babble.Preview(200) // Longer than message
if shortPreview != longMessage { if shortPreview != longMessage {
t.Error("Expected short message to not be truncated") t.Error("Expected short message to not be truncated")
} }
// Test WordCount // Test WordCount
babble.Babble = "This is a test with five words" babble.Babble = "This is a test with five words"
wordCount := babble.WordCount() wordCount := babble.WordCount()
if wordCount != 7 { if wordCount != 7 {
t.Errorf("Expected 7 words, got %d", wordCount) t.Errorf("Expected 7 words, got %d", wordCount)
} }
// Test Length // Test Length
expectedLength := len(babble.Babble) expectedLength := len(babble.Babble)
if babble.Length() != expectedLength { if babble.Length() != expectedLength {
t.Errorf("Expected length %d, got %d", expectedLength, babble.Length()) t.Errorf("Expected length %d, got %d", expectedLength, babble.Length())
} }
// Test Contains // Test Contains
if !babble.Contains("test") { if !babble.Contains("test") {
t.Error("Expected message to contain 'test'") t.Error("Expected message to contain 'test'")
@ -534,23 +534,23 @@ func TestUtilityMethods(t *testing.T) {
if babble.Contains("nonexistent") { if babble.Contains("nonexistent") {
t.Error("Expected message not to contain 'nonexistent'") t.Error("Expected message not to contain 'nonexistent'")
} }
// Test IsEmpty // Test IsEmpty
babble.Babble = "" babble.Babble = ""
if !babble.IsEmpty() { if !babble.IsEmpty() {
t.Error("Expected empty message to be empty") t.Error("Expected empty message to be empty")
} }
babble.Babble = " " babble.Babble = " "
if !babble.IsEmpty() { if !babble.IsEmpty() {
t.Error("Expected whitespace-only message to be empty") t.Error("Expected whitespace-only message to be empty")
} }
babble.Babble = "Not empty" babble.Babble = "Not empty"
if babble.IsEmpty() { if babble.IsEmpty() {
t.Error("Expected non-empty message not to be empty") t.Error("Expected non-empty message not to be empty")
} }
// Test IsLongMessage // Test IsLongMessage
shortMsg := "Short" shortMsg := "Short"
babble.Babble = shortMsg babble.Babble = shortMsg
@ -565,22 +565,22 @@ func TestUtilityMethods(t *testing.T) {
func TestMentionMethods(t *testing.T) { func TestMentionMethods(t *testing.T) {
db := setupTestDB(t) db := setupTestDB(t)
defer db.Close() defer db.Close()
// Test GetMentions // Test GetMentions
babble, _ := Find(db, 2) // Bob's message: "Thanks Alice! @Alice this game is great" babble, _ := Find(db, 2) // Bob's message: "Thanks Alice! @Alice this game is great"
mentions := babble.GetMentions() mentions := babble.GetMentions()
expectedMentions := []string{"Alice"} expectedMentions := []string{"Alice"}
if len(mentions) != len(expectedMentions) { if len(mentions) != len(expectedMentions) {
t.Errorf("Expected %d mentions, got %d", len(expectedMentions), len(mentions)) t.Errorf("Expected %d mentions, got %d", len(expectedMentions), len(mentions))
} }
for i, expected := range expectedMentions { for i, expected := range expectedMentions {
if i < len(mentions) && mentions[i] != expected { if i < len(mentions) && mentions[i] != expected {
t.Errorf("Expected mention '%s' at position %d, got '%s'", expected, i, mentions[i]) t.Errorf("Expected mention '%s' at position %d, got '%s'", expected, i, mentions[i])
} }
} }
// Test HasMention // Test HasMention
if !babble.HasMention("Alice") { if !babble.HasMention("Alice") {
t.Error("Expected message to mention Alice") t.Error("Expected message to mention Alice")
@ -591,35 +591,35 @@ func TestMentionMethods(t *testing.T) {
if babble.HasMention("Bob") { if babble.HasMention("Bob") {
t.Error("Expected message not to mention Bob") t.Error("Expected message not to mention Bob")
} }
// Test message with multiple mentions and punctuation // Test message with multiple mentions and punctuation
babble.Babble = "Hey @Alice, @Bob! Can you help @Charlie?" babble.Babble = "Hey @Alice, @Bob! Can you help @Charlie?"
mentions = babble.GetMentions() mentions = babble.GetMentions()
expectedMentions = []string{"Alice", "Bob", "Charlie"} expectedMentions = []string{"Alice", "Bob", "Charlie"}
if len(mentions) != len(expectedMentions) { if len(mentions) != len(expectedMentions) {
t.Errorf("Expected %d mentions, got %d: %v", len(expectedMentions), len(mentions), mentions) t.Errorf("Expected %d mentions, got %d: %v", len(expectedMentions), len(mentions), mentions)
} }
for _, expected := range expectedMentions { for _, expected := range expectedMentions {
if !babble.HasMention(expected) { if !babble.HasMention(expected) {
t.Errorf("Expected message to mention %s", expected) t.Errorf("Expected message to mention %s", expected)
} }
} }
// Test message with no mentions // Test message with no mentions
babble.Babble = "No mentions in this message" babble.Babble = "No mentions in this message"
mentions = babble.GetMentions() mentions = babble.GetMentions()
if len(mentions) != 0 { if len(mentions) != 0 {
t.Errorf("Expected 0 mentions, got %d", len(mentions)) t.Errorf("Expected 0 mentions, got %d", len(mentions))
} }
// Test malformed mentions (should be ignored) // Test malformed mentions (should be ignored)
babble.Babble = "Just @ alone or @" babble.Babble = "Just @ alone or @"
mentions = babble.GetMentions() mentions = babble.GetMentions()
if len(mentions) != 0 { if len(mentions) != 0 {
t.Errorf("Expected 0 mentions for malformed @, got %d", len(mentions)) t.Errorf("Expected 0 mentions for malformed @, got %d", len(mentions))
} }
} }

View File

@ -45,13 +45,13 @@ func setupTestDB(t *testing.T) *database.DB {
(?, ?, 2, 4, 0, 'Re: Bug Reports', 'Found a small issue with spell casting.'), (?, ?, 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.')` (?, ?, 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*7, now - 86400*1, // Thread 1, last activity 1 day ago
now - 86400*6, now - 86400*6, // Reply 1 now - 86400*6, now - 86400*6, // Reply 1
now - 86400*1, now - 86400*1, // Reply 2 (most recent activity on thread 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*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 - 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 { if err := db.Exec(testForum, timestamps...); err != nil {
@ -662,4 +662,4 @@ func TestRelationshipMethods(t *testing.T) {
if threadSelf.ID != thread.ID { if threadSelf.ID != thread.ID {
t.Errorf("Expected GetThread on thread to return self, got ID %d", threadSelf.ID) t.Errorf("Expected GetThread on thread to return self, got ID %d", threadSelf.ID)
} }
} }

View File

@ -13,12 +13,12 @@ func setupTestDB(t *testing.T) *database.DB {
t.Cleanup(func() { t.Cleanup(func() {
os.Remove(testDB) os.Remove(testDB)
}) })
db, err := database.Open(testDB) db, err := database.Open(testDB)
if err != nil { if err != nil {
t.Fatalf("Failed to open test database: %v", err) t.Fatalf("Failed to open test database: %v", err)
} }
// Create news table // Create news table
createTable := `CREATE TABLE news ( createTable := `CREATE TABLE news (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -26,11 +26,11 @@ func setupTestDB(t *testing.T) *database.DB {
posted INTEGER NOT NULL DEFAULT (unixepoch()), posted INTEGER NOT NULL DEFAULT (unixepoch()),
content TEXT NOT NULL content TEXT NOT NULL
)` )`
if err := db.Exec(createTable); err != nil { if err := db.Exec(createTable); err != nil {
t.Fatalf("Failed to create news table: %v", err) t.Fatalf("Failed to create news table: %v", err)
} }
// Insert test data with specific timestamps for predictable testing // Insert test data with specific timestamps for predictable testing
now := time.Now().Unix() now := time.Now().Unix()
testNews := `INSERT INTO news (author, posted, content) VALUES testNews := `INSERT INTO news (author, posted, content) VALUES
@ -39,32 +39,32 @@ func setupTestDB(t *testing.T) *database.DB {
(1, ?, 'Third post - recent update'), (1, ?, 'Third post - recent update'),
(3, ?, 'Fourth post from admin'), (3, ?, 'Fourth post from admin'),
(2, ?, 'Fifth post - maintenance notice')` (2, ?, 'Fifth post - maintenance notice')`
timestamps := []interface{}{ timestamps := []any{
now - 86400*7, // 1 week ago 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*2, // 2 days ago
now - 86400*1, // 1 day ago now - 86400*1, // 1 day ago
now - 3600, // 1 hour ago now - 3600, // 1 hour ago
} }
if err := db.Exec(testNews, timestamps...); err != nil { if err := db.Exec(testNews, timestamps...); err != nil {
t.Fatalf("Failed to insert test news: %v", err) t.Fatalf("Failed to insert test news: %v", err)
} }
return db return db
} }
func TestFind(t *testing.T) { func TestFind(t *testing.T) {
db := setupTestDB(t) db := setupTestDB(t)
defer db.Close() defer db.Close()
// Test finding existing news // Test finding existing news
news, err := Find(db, 1) news, err := Find(db, 1)
if err != nil { if err != nil {
t.Fatalf("Failed to find news: %v", err) t.Fatalf("Failed to find news: %v", err)
} }
if news.ID != 1 { if news.ID != 1 {
t.Errorf("Expected ID 1, got %d", news.ID) t.Errorf("Expected ID 1, got %d", news.ID)
} }
@ -77,7 +77,7 @@ func TestFind(t *testing.T) {
if news.Posted == 0 { if news.Posted == 0 {
t.Error("Expected non-zero posted timestamp") t.Error("Expected non-zero posted timestamp")
} }
// Test finding non-existent news // Test finding non-existent news
_, err = Find(db, 999) _, err = Find(db, 999)
if err == nil { if err == nil {
@ -88,23 +88,23 @@ func TestFind(t *testing.T) {
func TestAll(t *testing.T) { func TestAll(t *testing.T) {
db := setupTestDB(t) db := setupTestDB(t)
defer db.Close() defer db.Close()
newsPosts, err := All(db) newsPosts, err := All(db)
if err != nil { if err != nil {
t.Fatalf("Failed to get all news: %v", err) t.Fatalf("Failed to get all news: %v", err)
} }
if len(newsPosts) != 5 { if len(newsPosts) != 5 {
t.Errorf("Expected 5 news posts, got %d", len(newsPosts)) t.Errorf("Expected 5 news posts, got %d", len(newsPosts))
} }
// Check ordering (newest first) // Check ordering (newest first)
if len(newsPosts) >= 2 { if len(newsPosts) >= 2 {
if newsPosts[0].Posted < newsPosts[1].Posted { if newsPosts[0].Posted < newsPosts[1].Posted {
t.Error("Expected news to be ordered by posted time (newest first)") t.Error("Expected news to be ordered by posted time (newest first)")
} }
} }
// First post should be the most recent (1 hour ago) // First post should be the most recent (1 hour ago)
if newsPosts[0].Content != "Fifth post - maintenance notice" { if newsPosts[0].Content != "Fifth post - maintenance notice" {
t.Errorf("Expected newest post first, got '%s'", newsPosts[0].Content) 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) { func TestByAuthor(t *testing.T) {
db := setupTestDB(t) db := setupTestDB(t)
defer db.Close() defer db.Close()
// Test posts by author 1 // Test posts by author 1
author1Posts, err := ByAuthor(db, 1) author1Posts, err := ByAuthor(db, 1)
if err != nil { if err != nil {
t.Fatalf("Failed to get news by author: %v", err) t.Fatalf("Failed to get news by author: %v", err)
} }
if len(author1Posts) != 2 { if len(author1Posts) != 2 {
t.Errorf("Expected 2 posts by author 1, got %d", len(author1Posts)) t.Errorf("Expected 2 posts by author 1, got %d", len(author1Posts))
} }
// Verify all posts are by author 1 // Verify all posts are by author 1
for _, post := range author1Posts { for _, post := range author1Posts {
if post.Author != 1 { if post.Author != 1 {
t.Errorf("Expected author 1, got %d", post.Author) t.Errorf("Expected author 1, got %d", post.Author)
} }
} }
// Check ordering (newest first) // Check ordering (newest first)
if len(author1Posts) == 2 { if len(author1Posts) == 2 {
if author1Posts[0].Content != "Third post - recent update" { if author1Posts[0].Content != "Third post - recent update" {
t.Errorf("Expected newest post by author 1 first") t.Errorf("Expected newest post by author 1 first")
} }
} }
// Test author with no posts // Test author with no posts
noPosts, err := ByAuthor(db, 999) noPosts, err := ByAuthor(db, 999)
if err != nil { if err != nil {
t.Fatalf("Failed to query non-existent author: %v", err) t.Fatalf("Failed to query non-existent author: %v", err)
} }
if len(noPosts) != 0 { if len(noPosts) != 0 {
t.Errorf("Expected 0 posts by non-existent author, got %d", len(noPosts)) 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) { func TestRecent(t *testing.T) {
db := setupTestDB(t) db := setupTestDB(t)
defer db.Close() defer db.Close()
// Test getting 3 most recent posts // Test getting 3 most recent posts
recentPosts, err := Recent(db, 3) recentPosts, err := Recent(db, 3)
if err != nil { if err != nil {
t.Fatalf("Failed to get recent news: %v", err) t.Fatalf("Failed to get recent news: %v", err)
} }
if len(recentPosts) != 3 { if len(recentPosts) != 3 {
t.Errorf("Expected 3 recent posts, got %d", len(recentPosts)) t.Errorf("Expected 3 recent posts, got %d", len(recentPosts))
} }
// Check ordering (newest first) // Check ordering (newest first)
if len(recentPosts) >= 2 { if len(recentPosts) >= 2 {
if recentPosts[0].Posted < recentPosts[1].Posted { if recentPosts[0].Posted < recentPosts[1].Posted {
t.Error("Expected recent posts to be ordered newest first") t.Error("Expected recent posts to be ordered newest first")
} }
} }
// Test getting more posts than exist // Test getting more posts than exist
allRecentPosts, err := Recent(db, 10) allRecentPosts, err := Recent(db, 10)
if err != nil { if err != nil {
t.Fatalf("Failed to get recent news with high limit: %v", err) t.Fatalf("Failed to get recent news with high limit: %v", err)
} }
if len(allRecentPosts) != 5 { if len(allRecentPosts) != 5 {
t.Errorf("Expected 5 posts (all available), got %d", len(allRecentPosts)) 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) { func TestSince(t *testing.T) {
db := setupTestDB(t) db := setupTestDB(t)
defer db.Close() defer db.Close()
// Test posts since 3 days ago // Test posts since 3 days ago
threeDaysAgo := time.Now().AddDate(0, 0, -3).Unix() threeDaysAgo := time.Now().AddDate(0, 0, -3).Unix()
recentPosts, err := Since(db, threeDaysAgo) recentPosts, err := Since(db, threeDaysAgo)
if err != nil { if err != nil {
t.Fatalf("Failed to get news since timestamp: %v", err) t.Fatalf("Failed to get news since timestamp: %v", err)
} }
// Should get posts from 2 days ago, 1 day ago, and 1 hour ago // Should get posts from 2 days ago, 1 day ago, and 1 hour ago
expectedCount := 3 expectedCount := 3
if len(recentPosts) != expectedCount { if len(recentPosts) != expectedCount {
t.Errorf("Expected %d posts since 3 days ago, got %d", expectedCount, len(recentPosts)) t.Errorf("Expected %d posts since 3 days ago, got %d", expectedCount, len(recentPosts))
} }
// Verify all posts are since the timestamp // Verify all posts are since the timestamp
for _, post := range recentPosts { for _, post := range recentPosts {
if post.Posted < threeDaysAgo { if post.Posted < threeDaysAgo {
t.Errorf("Post with timestamp %d is before the 'since' timestamp %d", 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) // Test with future timestamp (should return no posts)
futurePosts, err := Since(db, time.Now().AddDate(0, 0, 1).Unix()) futurePosts, err := Since(db, time.Now().AddDate(0, 0, 1).Unix())
if err != nil { if err != nil {
t.Fatalf("Failed to query future timestamp: %v", err) t.Fatalf("Failed to query future timestamp: %v", err)
} }
if len(futurePosts) != 0 { if len(futurePosts) != 0 {
t.Errorf("Expected 0 posts since future timestamp, got %d", len(futurePosts)) 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) { func TestBetween(t *testing.T) {
db := setupTestDB(t) db := setupTestDB(t)
defer db.Close() defer db.Close()
// Test posts between 6 days ago and 1 day ago // Test posts between 6 days ago and 1 day ago
start := time.Now().AddDate(0, 0, -6).Unix() start := time.Now().AddDate(0, 0, -6).Unix()
end := time.Now().AddDate(0, 0, -1).Unix() end := time.Now().AddDate(0, 0, -1).Unix()
betweenPosts, err := Between(db, start, end) betweenPosts, err := Between(db, start, end)
if err != nil { if err != nil {
t.Fatalf("Failed to get news between timestamps: %v", err) t.Fatalf("Failed to get news between timestamps: %v", err)
} }
// Should get posts from 5 days ago, 2 days ago, and 1 day ago // Should get posts from 5 days ago, 2 days ago, and 1 day ago
expectedCount := 3 expectedCount := 3
if len(betweenPosts) != expectedCount { if len(betweenPosts) != expectedCount {
t.Errorf("Expected %d posts between timestamps, got %d", expectedCount, len(betweenPosts)) t.Errorf("Expected %d posts between timestamps, got %d", expectedCount, len(betweenPosts))
} }
// Verify all posts are within the range // Verify all posts are within the range
for _, post := range betweenPosts { for _, post := range betweenPosts {
if post.Posted < start || post.Posted > end { if post.Posted < start || post.Posted > end {
t.Errorf("Post with timestamp %d is outside range [%d, %d]", post.Posted, start, end) t.Errorf("Post with timestamp %d is outside range [%d, %d]", post.Posted, start, end)
} }
} }
// Test with narrow range (should return fewer posts) // Test with narrow range (should return fewer posts)
narrowStart := time.Now().AddDate(0, 0, -2).Unix() narrowStart := time.Now().AddDate(0, 0, -2).Unix()
narrowEnd := time.Now().AddDate(0, 0, -1).Unix() narrowEnd := time.Now().AddDate(0, 0, -1).Unix()
narrowPosts, err := Between(db, narrowStart, narrowEnd) narrowPosts, err := Between(db, narrowStart, narrowEnd)
if err != nil { if err != nil {
t.Fatalf("Failed to get news in narrow range: %v", err) t.Fatalf("Failed to get news in narrow range: %v", err)
} }
if len(narrowPosts) != 2 { // 2 days ago and 1 day ago if len(narrowPosts) != 2 { // 2 days ago and 1 day ago
t.Errorf("Expected 2 posts in narrow range, got %d", len(narrowPosts)) 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) { func TestBuilder(t *testing.T) {
db := setupTestDB(t) db := setupTestDB(t)
defer db.Close() defer db.Close()
// Create new news using builder // Create new news using builder
testTime := time.Now() testTime := time.Now()
news, err := NewBuilder(db). news, err := NewBuilder(db).
@ -268,11 +268,11 @@ func TestBuilder(t *testing.T) {
WithContent("Test news content from builder"). WithContent("Test news content from builder").
WithPostedTime(testTime). WithPostedTime(testTime).
Create() Create()
if err != nil { if err != nil {
t.Fatalf("Failed to create news with builder: %v", err) t.Fatalf("Failed to create news with builder: %v", err)
} }
if news.ID == 0 { if news.ID == 0 {
t.Error("Expected non-zero ID after creation") t.Error("Expected non-zero ID after creation")
} }
@ -285,27 +285,27 @@ func TestBuilder(t *testing.T) {
if news.Posted != testTime.Unix() { if news.Posted != testTime.Unix() {
t.Errorf("Expected posted time %d, got %d", testTime.Unix(), news.Posted) t.Errorf("Expected posted time %d, got %d", testTime.Unix(), news.Posted)
} }
// Verify it was saved to database // Verify it was saved to database
foundNews, err := Find(db, news.ID) foundNews, err := Find(db, news.ID)
if err != nil { if err != nil {
t.Fatalf("Failed to find created news: %v", err) t.Fatalf("Failed to find created news: %v", err)
} }
if foundNews.Content != "Test news content from builder" { if foundNews.Content != "Test news content from builder" {
t.Errorf("Created news not found in database") t.Errorf("Created news not found in database")
} }
// Test builder with default timestamp // Test builder with default timestamp
defaultNews, err := NewBuilder(db). defaultNews, err := NewBuilder(db).
WithAuthor(1). WithAuthor(1).
WithContent("News with default timestamp"). WithContent("News with default timestamp").
Create() Create()
if err != nil { if err != nil {
t.Fatalf("Failed to create news with default timestamp: %v", err) t.Fatalf("Failed to create news with default timestamp: %v", err)
} }
// Should have recent timestamp (within last minute) // Should have recent timestamp (within last minute)
if time.Since(defaultNews.PostedTime()) > time.Minute { if time.Since(defaultNews.PostedTime()) > time.Minute {
t.Error("Expected default timestamp to be recent") t.Error("Expected default timestamp to be recent")
@ -315,29 +315,29 @@ func TestBuilder(t *testing.T) {
func TestSave(t *testing.T) { func TestSave(t *testing.T) {
db := setupTestDB(t) db := setupTestDB(t)
defer db.Close() defer db.Close()
news, err := Find(db, 1) news, err := Find(db, 1)
if err != nil { if err != nil {
t.Fatalf("Failed to find news: %v", err) t.Fatalf("Failed to find news: %v", err)
} }
// Modify news // Modify news
news.Author = 999 news.Author = 999
news.Content = "Updated content" news.Content = "Updated content"
news.Posted = time.Now().Unix() news.Posted = time.Now().Unix()
// Save changes // Save changes
err = news.Save() err = news.Save()
if err != nil { if err != nil {
t.Fatalf("Failed to save news: %v", err) t.Fatalf("Failed to save news: %v", err)
} }
// Verify changes were saved // Verify changes were saved
updatedNews, err := Find(db, 1) updatedNews, err := Find(db, 1)
if err != nil { if err != nil {
t.Fatalf("Failed to find updated news: %v", err) t.Fatalf("Failed to find updated news: %v", err)
} }
if updatedNews.Author != 999 { if updatedNews.Author != 999 {
t.Errorf("Expected updated author 999, got %d", updatedNews.Author) t.Errorf("Expected updated author 999, got %d", updatedNews.Author)
} }
@ -349,18 +349,18 @@ func TestSave(t *testing.T) {
func TestDelete(t *testing.T) { func TestDelete(t *testing.T) {
db := setupTestDB(t) db := setupTestDB(t)
defer db.Close() defer db.Close()
news, err := Find(db, 1) news, err := Find(db, 1)
if err != nil { if err != nil {
t.Fatalf("Failed to find news: %v", err) t.Fatalf("Failed to find news: %v", err)
} }
// Delete news // Delete news
err = news.Delete() err = news.Delete()
if err != nil { if err != nil {
t.Fatalf("Failed to delete news: %v", err) t.Fatalf("Failed to delete news: %v", err)
} }
// Verify news was deleted // Verify news was deleted
_, err = Find(db, 1) _, err = Find(db, 1)
if err == nil { if err == nil {
@ -371,48 +371,48 @@ func TestDelete(t *testing.T) {
func TestUtilityMethods(t *testing.T) { func TestUtilityMethods(t *testing.T) {
db := setupTestDB(t) db := setupTestDB(t)
defer db.Close() defer db.Close()
news, _ := Find(db, 1) news, _ := Find(db, 1)
// Test PostedTime // Test PostedTime
postedTime := news.PostedTime() postedTime := news.PostedTime()
if postedTime.IsZero() { if postedTime.IsZero() {
t.Error("Expected non-zero posted time") t.Error("Expected non-zero posted time")
} }
// Test IsRecent (should be false for old posts initially) // Test IsRecent (should be false for old posts initially)
if news.IsRecent() { if news.IsRecent() {
t.Error("Expected old news post not to be recent") t.Error("Expected old news post not to be recent")
} }
// Test SetPostedTime // Test SetPostedTime
newTime := time.Now().Add(-2 * time.Hour) newTime := time.Now().Add(-2 * time.Hour)
news.SetPostedTime(newTime) news.SetPostedTime(newTime)
if news.Posted != newTime.Unix() { if news.Posted != newTime.Unix() {
t.Errorf("Expected posted timestamp %d, got %d", newTime.Unix(), news.Posted) t.Errorf("Expected posted timestamp %d, got %d", newTime.Unix(), news.Posted)
} }
// Test IsRecent (should be true after setting to 2 hours ago) // Test IsRecent (should be true after setting to 2 hours ago)
if !news.IsRecent() { if !news.IsRecent() {
t.Error("Expected news post from 2 hours ago to be recent") t.Error("Expected news post from 2 hours ago to be recent")
} }
// Create recent post // Create recent post
recentNews, _ := NewBuilder(db). recentNews, _ := NewBuilder(db).
WithAuthor(1). WithAuthor(1).
WithContent("Recent post"). WithContent("Recent post").
Create() Create()
if !recentNews.IsRecent() { if !recentNews.IsRecent() {
t.Error("Expected newly created post to be recent") t.Error("Expected newly created post to be recent")
} }
// Test Age // Test Age
age := news.Age() age := news.Age()
if age < 0 { if age < 0 {
t.Error("Expected positive age") t.Error("Expected positive age")
} }
// Test IsAuthor // Test IsAuthor
if !news.IsAuthor(news.Author) { if !news.IsAuthor(news.Author) {
t.Error("Expected IsAuthor to return true for correct author") t.Error("Expected IsAuthor to return true for correct author")
@ -420,11 +420,11 @@ func TestUtilityMethods(t *testing.T) {
if news.IsAuthor(999) { if news.IsAuthor(999) {
t.Error("Expected IsAuthor to return false for incorrect author") t.Error("Expected IsAuthor to return false for incorrect author")
} }
// Test Preview // Test Preview
longContent := "This is a very long content that should be truncated when preview is called" longContent := "This is a very long content that should be truncated when preview is called"
news.Content = longContent news.Content = longContent
preview := news.Preview(20) preview := news.Preview(20)
if len(preview) > 20 { if len(preview) > 20 {
t.Errorf("Expected preview length <= 20, got %d", len(preview)) t.Errorf("Expected preview length <= 20, got %d", len(preview))
@ -432,28 +432,28 @@ func TestUtilityMethods(t *testing.T) {
if preview[len(preview)-3:] != "..." { if preview[len(preview)-3:] != "..." {
t.Error("Expected preview to end with ellipsis") t.Error("Expected preview to end with ellipsis")
} }
shortPreview := news.Preview(100) // Longer than content shortPreview := news.Preview(100) // Longer than content
if shortPreview != longContent { if shortPreview != longContent {
t.Error("Expected short content to not be truncated") t.Error("Expected short content to not be truncated")
} }
// Test WordCount // Test WordCount
news.Content = "This is a test with five words" news.Content = "This is a test with five words"
wordCount := news.WordCount() wordCount := news.WordCount()
if wordCount != 7 { if wordCount != 7 {
t.Errorf("Expected 7 words, got %d", wordCount) t.Errorf("Expected 7 words, got %d", wordCount)
} }
news.Content = "" news.Content = ""
emptyWordCount := news.WordCount() emptyWordCount := news.WordCount()
if emptyWordCount != 0 { if emptyWordCount != 0 {
t.Errorf("Expected 0 words for empty content, got %d", emptyWordCount) t.Errorf("Expected 0 words for empty content, got %d", emptyWordCount)
} }
news.Content = "OneWord" news.Content = "OneWord"
oneWordCount := news.WordCount() oneWordCount := news.WordCount()
if oneWordCount != 1 { if oneWordCount != 1 {
t.Errorf("Expected 1 word, got %d", oneWordCount) t.Errorf("Expected 1 word, got %d", oneWordCount)
} }
} }

View File

@ -3,17 +3,72 @@ package server
import ( import (
"fmt" "fmt"
"log" "log"
"os"
"path/filepath"
"dk/internal/router"
"dk/internal/template"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
) )
func Start() error { func Start(port string) error {
// Simple fasthttp server for now // Initialize template cache - use current working directory for development
requestHandler := func(ctx *fasthttp.RequestCtx) { cwd, err := os.Getwd()
ctx.SetContentType("text/html; charset=utf-8") if err != nil {
fmt.Fprintf(ctx, "<h1>Dragon Knight</h1><p>Server is running!</p>") return fmt.Errorf("failed to get current working directory: %w", err)
} }
templateCache := template.NewCache(cwd)
log.Println("Server starting on :8080")
return fasthttp.ListenAndServe(":8080", requestHandler) // Initialize router
r := router.New()
// Hello world endpoint
r.Get("/", func(ctx router.Ctx, params []string) {
tmpl, err := templateCache.Load("hello.html")
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
fmt.Fprintf(ctx, "Template error: %v", err)
return
}
data := map[string]any{
"title": "Dragon Knight",
"message": "Hello World!",
}
tmpl.WriteTo(ctx, data)
})
// Use current working directory for static files
assetsDir := filepath.Join(cwd, "assets")
// Static file server for /assets
fs := &fasthttp.FS{
Root: assetsDir,
Compress: true,
}
assetsHandler := fs.NewRequestHandler()
// Combined handler
requestHandler := func(ctx *fasthttp.RequestCtx) {
path := string(ctx.Path())
// Handle static assets - strip /assets prefix
if len(path) >= 7 && path[:7] == "/assets" {
// Strip the /assets prefix for the file system handler
originalPath := ctx.Path()
ctx.Request.URI().SetPath(path[7:]) // Remove "/assets" prefix
assetsHandler(ctx)
ctx.Request.URI().SetPathBytes(originalPath) // Restore original path
return
}
// Handle routes
r.ServeHTTP(ctx)
}
addr := ":" + port
log.Printf("Server starting on %s", addr)
return fasthttp.ListenAndServe(addr, requestHandler)
} }

75
internal/template/doc.go Normal file
View File

@ -0,0 +1,75 @@
// Package template provides a high-performance template engine with in-memory
// caching, automatic reloading, and advanced template composition features.
//
// # Basic Usage
//
// cache := template.NewCache("") // Auto-detects binary location
// tmpl, err := cache.Load("page.html")
// if err != nil {
// log.Fatal(err)
// }
//
// data := map[string]any{
// "title": "Welcome",
// "user": map[string]any{
// "name": "Alice",
// "email": "alice@example.com",
// },
// }
//
// result := tmpl.RenderNamed(data)
//
// # Placeholder Types
//
// Named placeholders: {name}, {title}
//
// Dot notation: {user.name}, {user.contact.email}
//
// Positional: {0}, {1}, {2}
//
// # Template Composition
//
// Includes - embed other templates with data sharing:
//
// {include "header.html"}
// {include "nav.html"}
//
// Blocks - define reusable content sections:
//
// {block "content"}
// <h1>Default content</h1>
// {/block}
//
// Yield - template inheritance points:
//
// <main>{yield content}</main>
// <footer>{yield footer}</footer>
//
// # Template Inheritance Example
//
// layout.html:
//
// <!DOCTYPE html>
// <html>
// <head><title>{title}</title></head>
// <body>{yield content}</body>
// </html>
//
// page.html:
//
// {include "layout.html"}
// {block "content"}
// <h1>Welcome {user.name}!</h1>
// {/block}
//
// # Advanced Features
//
// Disable includes for partial rendering:
//
// opts := RenderOptions{ResolveIncludes: false}
// chunk := tmpl.RenderNamedWithOptions(opts, data)
//
// FastHTTP integration:
//
// tmpl.WriteTo(ctx, data) // Sets content-type and writes response
package template

View File

@ -0,0 +1,358 @@
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 RenderOptions struct {
ResolveIncludes bool
Blocks map[string]string
}
type Template struct {
name string
content string
modTime time.Time
filePath string
cache *Cache
}
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,
cache: c,
}
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 {
return t.RenderPositionalWithOptions(RenderOptions{ResolveIncludes: true}, args...)
}
func (t *Template) RenderPositionalWithOptions(opts RenderOptions, 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))
}
if opts.ResolveIncludes {
result = t.processIncludes(result, nil, opts)
}
return result
}
func (t *Template) RenderNamed(data map[string]any) string {
return t.RenderNamedWithOptions(RenderOptions{ResolveIncludes: true}, data)
}
func (t *Template) RenderNamedWithOptions(opts RenderOptions, data map[string]any) string {
result := t.content
// Process blocks first to extract them
result = t.processBlocks(result, &opts)
// Process includes next so they get the data substitutions
if opts.ResolveIncludes {
result = t.processIncludes(result, data, opts)
}
// Apply data substitutions after includes are processed
for key, value := range data {
placeholder := fmt.Sprintf("{%s}", key)
result = strings.ReplaceAll(result, placeholder, fmt.Sprintf("%v", value))
}
result = t.replaceDotNotation(result, data)
result = t.processYield(result, opts)
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) {
t.WriteToWithOptions(ctx, data, RenderOptions{ResolveIncludes: true})
}
func (t *Template) WriteToWithOptions(ctx *fasthttp.RequestCtx, data any, opts RenderOptions) {
var result string
switch v := data.(type) {
case map[string]any:
result = t.RenderNamedWithOptions(opts, v)
case []any:
result = t.RenderPositionalWithOptions(opts, 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.RenderPositionalWithOptions(opts, args...)
} else {
result = t.RenderPositionalWithOptions(opts, data)
}
}
ctx.SetContentType("text/html; charset=utf-8")
ctx.WriteString(result)
}
// processIncludes handles {include "template.html"} directives
func (t *Template) processIncludes(content string, data map[string]any, opts RenderOptions) string {
result := content
for {
start := strings.Index(result, "{include ")
if start == -1 {
break
}
end := strings.Index(result[start:], "}")
if end == -1 {
break
}
end += start
directive := result[start+9:end] // Skip "{include "
templateName := strings.Trim(directive, "\" ")
if includedTemplate, err := t.cache.Load(templateName); err == nil {
var includedContent string
if data != nil {
// Create new options to pass blocks to included template
includeOpts := RenderOptions{
ResolveIncludes: opts.ResolveIncludes,
Blocks: opts.Blocks,
}
includedContent = includedTemplate.RenderNamedWithOptions(includeOpts, data)
} else {
includedContent = includedTemplate.content
}
result = result[:start] + includedContent + result[end+1:]
} else {
// Remove the include directive if template not found
result = result[:start] + result[end+1:]
}
}
return result
}
// processYield handles {yield} directives for template inheritance
func (t *Template) processYield(content string, opts RenderOptions) string {
if opts.Blocks == nil {
return strings.ReplaceAll(content, "{yield}", "")
}
result := content
for blockName, blockContent := range opts.Blocks {
yieldPlaceholder := fmt.Sprintf("{yield %s}", blockName)
result = strings.ReplaceAll(result, yieldPlaceholder, blockContent)
}
// Replace any remaining {yield} with empty string
result = strings.ReplaceAll(result, "{yield}", "")
return result
}
// processBlocks extracts {block "name"}...{/block} sections
func (t *Template) processBlocks(content string, opts *RenderOptions) string {
if opts.Blocks == nil {
opts.Blocks = make(map[string]string)
}
result := content
for {
start := strings.Index(result, "{block ")
if start == -1 {
break
}
nameEnd := strings.Index(result[start:], "}")
if nameEnd == -1 {
break
}
nameEnd += start
blockName := strings.Trim(result[start+7:nameEnd], "\" ")
contentStart := nameEnd + 1
endTag := "{/block}"
contentEnd := strings.Index(result[contentStart:], endTag)
if contentEnd == -1 {
break
}
contentEnd += contentStart
blockContent := result[contentStart:contentEnd]
opts.Blocks[blockName] = blockContent
// Remove the block definition from the template
result = result[:start] + result[contentEnd+len(endTag):]
}
return result
}

View File

@ -0,0 +1,348 @@
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)
}
}
func TestIncludeSupport(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)
}
// Create main template with include
mainTemplate := `<html><head>{include "header.html"}</head><body>Hello {name}!</body></html>`
err = os.WriteFile(filepath.Join(templatesDir, "main.html"), []byte(mainTemplate), 0644)
if err != nil {
t.Fatal(err)
}
// Create included template
headerTemplate := `<title>{title}</title><meta charset="utf-8">`
err = os.WriteFile(filepath.Join(templatesDir, "header.html"), []byte(headerTemplate), 0644)
if err != nil {
t.Fatal(err)
}
cache := NewCache(tmpDir)
tmpl, err := cache.Load("main.html")
if err != nil {
t.Fatal(err)
}
data := map[string]any{
"name": "Alice",
"title": "Welcome",
}
result := tmpl.RenderNamed(data)
expected := "<html><head><title>Welcome</title><meta charset=\"utf-8\"></head><body>Hello Alice!</body></html>"
if result != expected {
t.Errorf("Expected %q, got %q", expected, result)
}
}
func TestIncludeDisabled(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)
}
mainTemplate := `<div>{include "partial.html"}</div>`
err = os.WriteFile(filepath.Join(templatesDir, "main.html"), []byte(mainTemplate), 0644)
if err != nil {
t.Fatal(err)
}
cache := NewCache(tmpDir)
tmpl, err := cache.Load("main.html")
if err != nil {
t.Fatal(err)
}
opts := RenderOptions{ResolveIncludes: false}
result := tmpl.RenderNamedWithOptions(opts, map[string]any{})
expected := `<div>{include "partial.html"}</div>`
if result != expected {
t.Errorf("Expected %q, got %q", expected, result)
}
}
func TestBlocksAndYield(t *testing.T) {
tmpl := &Template{
name: "test",
content: `{block "content"}Default content{/block}<main>{yield content}</main>`,
}
result := tmpl.RenderNamed(map[string]any{})
expected := "<main>Default content</main>"
if result != expected {
t.Errorf("Expected %q, got %q", expected, result)
}
}
func TestTemplateInheritance(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)
}
// Layout template
layoutTemplate := `<!DOCTYPE html><html><head><title>{title}</title></head><body>{yield content}</body></html>`
err = os.WriteFile(filepath.Join(templatesDir, "layout.html"), []byte(layoutTemplate), 0644)
if err != nil {
t.Fatal(err)
}
// Page template that extends layout
pageTemplate := `{include "layout.html"}{block "content"}<h1>Welcome {name}!</h1><p>This is the content block.</p>{/block}`
err = os.WriteFile(filepath.Join(templatesDir, "page.html"), []byte(pageTemplate), 0644)
if err != nil {
t.Fatal(err)
}
cache := NewCache(tmpDir)
tmpl, err := cache.Load("page.html")
if err != nil {
t.Fatal(err)
}
data := map[string]any{
"title": "Test Page",
"name": "Bob",
}
result := tmpl.RenderNamed(data)
expected := `<!DOCTYPE html><html><head><title>Test Page</title></head><body><h1>Welcome Bob!</h1><p>This is the content block.</p></body></html>`
if result != expected {
t.Errorf("Expected %q, got %q", expected, result)
}
}

View File

@ -71,12 +71,12 @@ The builder automatically sets sensible defaults for all fields if not specified
## Authentication and Verification ## Authentication and Verification
user, _ := users.Find(db, userID) user, _ := users.Find(db, userID)
// Check verification status // Check verification status
if user.IsVerified() { if user.IsVerified() {
fmt.Println("User email is verified") fmt.Println("User email is verified")
} }
// Check authorization levels // Check authorization levels
if user.IsAdmin() { if user.IsAdmin() {
fmt.Println("User has admin privileges") 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 // Update last online time
user.UpdateLastOnline() user.UpdateLastOnline()
user.Save() user.Save()
// Get activity information // Get activity information
registered := user.RegisteredTime() registered := user.RegisteredTime()
lastOnline := user.LastOnlineTime() lastOnline := user.LastOnlineTime()
fmt.Printf("Registered: %s\n", registered.Format("Jan 2, 2006")) fmt.Printf("Registered: %s\n", registered.Format("Jan 2, 2006"))
fmt.Printf("Last online: %s\n", lastOnline.Format("Jan 2 15:04")) 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 ## Stats and Progression
user, _ := users.Find(db, userID) user, _ := users.Find(db, userID)
// Get character stats // Get character stats
stats := user.GetStats() stats := user.GetStats()
fmt.Printf("Level %d: HP %d/%d, MP %d/%d\n", fmt.Printf("Level %d: HP %d/%d, MP %d/%d\n",
stats["level"], stats["hp"], stats["max_hp"], stats["level"], stats["hp"], stats["max_hp"],
stats["mp"], stats["max_mp"]) stats["mp"], stats["max_mp"])
// Update character progression // Update character progression
user.Level = 10 user.Level = 10
user.Exp = 5000 user.Exp = 5000
@ -122,7 +122,7 @@ The builder automatically sets sensible defaults for all fields if not specified
// Get current position // Get current position
x, y := user.GetPosition() x, y := user.GetPosition()
fmt.Printf("Player at (%d, %d)\n", x, y) fmt.Printf("Player at (%d, %d)\n", x, y)
// Move player // Move player
user.SetPosition(newX, newY) user.SetPosition(newX, newY)
user.Currently = "Exploring the forest" user.Currently = "Exploring the forest"
@ -131,10 +131,10 @@ The builder automatically sets sensible defaults for all fields if not specified
## Combat Status ## Combat Status
if user.IsFighting() { 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) user.MonsterID, user.MonsterHP)
} }
if user.IsAlive() { if user.IsAlive() {
fmt.Printf("Player has %d HP remaining\n", user.HP) 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 ## Spell Management
user, _ := users.Find(db, userID) user, _ := users.Find(db, userID)
// Get known spells // Get known spells
spells := user.GetSpellIDs() spells := user.GetSpellIDs()
fmt.Printf("Player knows %d spells: %v\n", len(spells), spells) fmt.Printf("Player knows %d spells: %v\n", len(spells), spells)
// Check if player knows a specific spell // Check if player knows a specific spell
if user.HasSpell("5") { if user.HasSpell("5") {
fmt.Println("Player knows spell 5") fmt.Println("Player knows spell 5")
} }
// Learn new spells // Learn new spells
newSpells := append(spells, "7", "8") newSpells := append(spells, "7", "8")
user.SetSpellIDs(newSpells) user.SetSpellIDs(newSpells)
@ -166,11 +166,11 @@ The builder automatically sets sensible defaults for all fields if not specified
if err != nil { if err != nil {
return err return err
} }
if !user.HasSpell(spellID) { if !user.HasSpell(spellID) {
return fmt.Errorf("user doesn't know spell %s", spellID) return fmt.Errorf("user doesn't know spell %s", spellID)
} }
// Spell casting logic here... // Spell casting logic here...
return nil return nil
} }
@ -180,16 +180,16 @@ The builder automatically sets sensible defaults for all fields if not specified
## Town Visits ## Town Visits
user, _ := users.Find(db, userID) user, _ := users.Find(db, userID)
// Get visited towns // Get visited towns
towns := user.GetTownIDs() towns := user.GetTownIDs()
fmt.Printf("Visited %d towns: %v\n", len(towns), towns) fmt.Printf("Visited %d towns: %v\n", len(towns), towns)
// Check if player has visited a town // Check if player has visited a town
if user.HasVisitedTown("3") { if user.HasVisitedTown("3") {
fmt.Println("Player has been to town 3") fmt.Println("Player has been to town 3")
} }
// Visit new town // Visit new town
visitedTowns := append(towns, "4") visitedTowns := append(towns, "4")
user.SetTownIDs(visitedTowns) user.SetTownIDs(visitedTowns)
@ -202,13 +202,13 @@ The builder automatically sets sensible defaults for all fields if not specified
if err != nil { if err != nil {
return err return err
} }
// Add town to visited list if not already there // Add town to visited list if not already there
if !user.HasVisitedTown(townID) { if !user.HasVisitedTown(townID) {
towns := user.GetTownIDs() towns := user.GetTownIDs()
user.SetTownIDs(append(towns, townID)) user.SetTownIDs(append(towns, townID))
} }
// Update position and status // Update position and status
// town coordinates would be looked up here // town coordinates would be looked up here
user.Currently = fmt.Sprintf("In town %s", townID) 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 ## Equipment Management
user, _ := users.Find(db, userID) user, _ := users.Find(db, userID)
// Get all equipment // Get all equipment
equipment := user.GetEquipment() equipment := user.GetEquipment()
weapon := equipment["weapon"].(map[string]interface{}) weapon := equipment["weapon"].(map[string]any)
armor := equipment["armor"].(map[string]interface{}) armor := equipment["armor"].(map[string]any)
fmt.Printf("Weapon: %s (ID: %d)\n", weapon["name"], weapon["id"]) fmt.Printf("Weapon: %s (ID: %d)\n", weapon["name"], weapon["id"])
fmt.Printf("Armor: %s (ID: %d)\n", armor["name"], armor["id"]) fmt.Printf("Armor: %s (ID: %d)\n", armor["name"], armor["id"])
// Equip new items // Equip new items
user.WeaponID = 15 user.WeaponID = 15
user.WeaponName = "Dragon Sword" user.WeaponName = "Dragon Sword"
@ -245,7 +245,7 @@ The builder automatically sets sensible defaults for all fields if not specified
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
fmt.Printf("Level 5 players (%d):\n", len(level5Players)) fmt.Printf("Level 5 players (%d):\n", len(level5Players))
for _, player := range level5Players { for _, player := range level5Players {
fmt.Printf("- %s (EXP: %d)\n", player.Username, player.Exp) 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 { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
fmt.Printf("Players online in last hour (%d):\n", len(onlinePlayers)) fmt.Printf("Players online in last hour (%d):\n", len(onlinePlayers))
for _, player := range onlinePlayers { for _, player := range onlinePlayers {
lastSeen := time.Since(player.LastOnlineTime()) 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) { func levelUpCharacter(user *users.User, newLevel int) {
user.Level = newLevel user.Level = newLevel
// Increase base stats // Increase base stats
user.MaxHP += 5 user.MaxHP += 5
user.HP = user.MaxHP // Full heal on level up user.HP = user.MaxHP // Full heal on level up
user.MaxMP += 2 user.MaxMP += 2
user.MP = user.MaxMP user.MP = user.MaxMP
// Stat bonuses // Stat bonuses
user.Strength++ user.Strength++
user.Attack++ user.Attack++
user.Defense++ user.Defense++
user.Save() user.Save()
} }
@ -353,23 +353,23 @@ The users table contains extensive character and game state information:
if user.IsFighting() { if user.IsFighting() {
return fmt.Errorf("already in combat") return fmt.Errorf("already in combat")
} }
user.Fighting = 1 user.Fighting = 1
user.MonsterID = monsterID user.MonsterID = monsterID
// monster HP would be looked up from monsters table // monster HP would be looked up from monsters table
user.MonsterHP = 50 user.MonsterHP = 50
user.Currently = "Fighting" user.Currently = "Fighting"
return user.Save() return user.Save()
} }
func endCombat(user *users.User, won bool) error { func endCombat(user *users.User, won bool) error {
user.Fighting = 0 user.Fighting = 0
user.MonsterID = 0 user.MonsterID = 0
user.MonsterHP = 0 user.MonsterHP = 0
user.MonsterSleep = 0 user.MonsterSleep = 0
user.MonsterImmune = 0 user.MonsterImmune = 0
if won { if won {
user.Currently = "Victorious" user.Currently = "Victorious"
// Award experience and gold // Award experience and gold
@ -377,7 +377,7 @@ The users table contains extensive character and game state information:
user.Currently = "Defeated" user.Currently = "Defeated"
user.HP = 0 // Player defeated user.HP = 0 // Player defeated
} }
return user.Save() return user.Save()
} }
@ -388,24 +388,24 @@ The users table contains extensive character and game state information:
if err != nil { if err != nil {
return err return err
} }
user.Auth = authLevel user.Auth = authLevel
return user.Save() return user.Save()
} }
func getUsersByAuthLevel(db *database.DB, minAuth int) ([]*users.User, error) { func getUsersByAuthLevel(db *database.DB, minAuth int) ([]*users.User, error) {
allUsers, err := users.All(db) allUsers, err := users.All(db)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var authorizedUsers []*users.User var authorizedUsers []*users.User
for _, user := range allUsers { for _, user := range allUsers {
if user.Auth >= minAuth { if user.Auth >= minAuth {
authorizedUsers = append(authorizedUsers, user) authorizedUsers = append(authorizedUsers, user)
} }
} }
return authorizedUsers, nil return authorizedUsers, nil
} }
@ -418,7 +418,7 @@ The users table is large and frequently accessed. Consider:
// Use specific lookups when possible // Use specific lookups when possible
user, _ := users.ByUsername(db, username) // Uses index user, _ := users.ByUsername(db, username) // Uses index
user, _ := users.ByEmail(db, email) // Uses index user, _ := users.ByEmail(db, email) // Uses index
// Limit results for admin interfaces // Limit results for admin interfaces
onlineUsers, _ := users.Online(db, time.Hour) // Bounded by time onlineUsers, _ := users.Online(db, time.Hour) // Bounded by time
levelUsers, _ := users.ByLevel(db, targetLevel) // Bounded by level 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 users map[int]*users.User
mutex sync.RWMutex mutex sync.RWMutex
} }
func (c *UserCache) GetUser(db *database.DB, id int) (*users.User, error) { func (c *UserCache) GetUser(db *database.DB, id int) (*users.User, error) {
c.mutex.RLock() c.mutex.RLock()
if user, ok := c.users[id]; ok { if user, ok := c.users[id]; ok {
@ -438,16 +438,16 @@ The users table is large and frequently accessed. Consider:
return user, nil return user, nil
} }
c.mutex.RUnlock() c.mutex.RUnlock()
user, err := users.Find(db, id) user, err := users.Find(db, id)
if err != nil { if err != nil {
return nil, err return nil, err
} }
c.mutex.Lock() c.mutex.Lock()
c.users[id] = user c.users[id] = user
c.mutex.Unlock() c.mutex.Unlock()
return user, nil return user, nil
} }
@ -460,20 +460,20 @@ The users table is large and frequently accessed. Consider:
if err != nil { if err != nil {
return nil, fmt.Errorf("user not found") return nil, fmt.Errorf("user not found")
} }
if !user.IsVerified() { if !user.IsVerified() {
return nil, fmt.Errorf("email not verified") return nil, fmt.Errorf("email not verified")
} }
// Verify password (implement password checking) // Verify password (implement password checking)
if !verifyPassword(user.Password, password) { if !verifyPassword(user.Password, password) {
return nil, fmt.Errorf("invalid password") return nil, fmt.Errorf("invalid password")
} }
// Update last online // Update last online
user.UpdateLastOnline() user.UpdateLastOnline()
user.Save() user.Save()
return user, nil return user, nil
} }
@ -485,16 +485,16 @@ The users table is large and frequently accessed. Consider:
user.HP = gameData.HP user.HP = gameData.HP
user.MP = gameData.MP user.MP = gameData.MP
user.Currently = gameData.Status user.Currently = gameData.Status
if gameData.InCombat { if gameData.InCombat {
user.Fighting = 1 user.Fighting = 1
user.MonsterID = gameData.MonsterID user.MonsterID = gameData.MonsterID
user.MonsterHP = gameData.MonsterHP user.MonsterHP = gameData.MonsterHP
} }
return user.Save() 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. 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

View File

@ -263,12 +263,12 @@ func (u *User) Save() error {
return fmt.Errorf("cannot save user without ID") 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 = ?, registered = ?, last_online = ?, auth = ?, x = ?, y = ?, class_id = ?, currently = ?,
fighting = ?, monster_id = ?, monster_hp = ?, monster_sleep = ?, monster_immune = ?, fighting = ?, monster_id = ?, monster_hp = ?, monster_sleep = ?, monster_immune = ?,
uber_damage = ?, uber_defense = ?, hp = ?, mp = ?, tp = ?, max_hp = ?, max_mp = ?, max_tp = ?, uber_damage = ?, uber_defense = ?, hp = ?, mp = ?, tp = ?, max_hp = ?, max_mp = ?, max_tp = ?,
level = ?, gold = ?, exp = ?, gold_bonus = ?, exp_bonus = ?, strength = ?, dexterity = ?, 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_2_id = ?, slot_3_id = ?, weapon_name = ?, armor_name = ?, shield_name = ?,
slot_1_name = ?, slot_2_name = ?, slot_3_name = ?, drop_code = ?, spells = ?, towns = ? slot_1_name = ?, slot_2_name = ?, slot_3_name = ?, drop_code = ?, spells = ?, towns = ?
WHERE id = ?` WHERE id = ?`
@ -382,32 +382,32 @@ func (u *User) HasVisitedTown(townID string) bool {
} }
// GetEquipment returns all equipped item information // GetEquipment returns all equipped item information
func (u *User) GetEquipment() map[string]interface{} { func (u *User) GetEquipment() map[string]any {
return map[string]interface{}{ return map[string]any{
"weapon": map[string]interface{}{"id": u.WeaponID, "name": u.WeaponName}, "weapon": map[string]any{"id": u.WeaponID, "name": u.WeaponName},
"armor": map[string]interface{}{"id": u.ArmorID, "name": u.ArmorName}, "armor": map[string]any{"id": u.ArmorID, "name": u.ArmorName},
"shield": map[string]interface{}{"id": u.ShieldID, "name": u.ShieldName}, "shield": map[string]any{"id": u.ShieldID, "name": u.ShieldName},
"slot1": map[string]interface{}{"id": u.Slot1ID, "name": u.Slot1Name}, "slot1": map[string]any{"id": u.Slot1ID, "name": u.Slot1Name},
"slot2": map[string]interface{}{"id": u.Slot2ID, "name": u.Slot2Name}, "slot2": map[string]any{"id": u.Slot2ID, "name": u.Slot2Name},
"slot3": map[string]interface{}{"id": u.Slot3ID, "name": u.Slot3Name}, "slot3": map[string]any{"id": u.Slot3ID, "name": u.Slot3Name},
} }
} }
// GetStats returns combat-relevant stats // GetStats returns combat-relevant stats
func (u *User) GetStats() map[string]int { func (u *User) GetStats() map[string]int {
return map[string]int{ return map[string]int{
"level": u.Level, "level": u.Level,
"hp": u.HP, "hp": u.HP,
"mp": u.MP, "mp": u.MP,
"tp": u.TP, "tp": u.TP,
"max_hp": u.MaxHP, "max_hp": u.MaxHP,
"max_mp": u.MaxMP, "max_mp": u.MaxMP,
"max_tp": u.MaxTP, "max_tp": u.MaxTP,
"strength": u.Strength, "strength": u.Strength,
"dexterity": u.Dexterity, "dexterity": u.Dexterity,
"attack": u.Attack, "attack": u.Attack,
"defense": u.Defense, "defense": u.Defense,
"uber_damage": u.UberDamage, "uber_damage": u.UberDamage,
"uber_defense": u.UberDefense, "uber_defense": u.UberDefense,
} }
} }
@ -421,4 +421,4 @@ func (u *User) GetPosition() (int, int) {
func (u *User) SetPosition(x, y int) { func (u *User) SetPosition(x, y int) {
u.X = x u.X = x
u.Y = y u.Y = y
} }

View File

@ -79,7 +79,7 @@ func setupTestDB(t *testing.T) *database.DB {
// Insert test data with specific timestamps // Insert test data with specific timestamps
now := time.Now().Unix() 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, x, y, class_id, level, gold, exp, hp, mp, tp, max_hp, max_mp, max_tp,
strength, dexterity, attack, defense, spells, towns) VALUES 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'), ('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, '', ''), ('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')` ('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{}{ timestamps := []any{
now - 86400*7, now - 3600*2, // alice: registered 1 week ago, last online 2 hours ago 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*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*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 now - 86400*30, now - 3600*1, // diana: registered 1 month ago, last online 1 hour ago
} }
if err := db.Exec(testUsers, timestamps...); err != nil { if err := db.Exec(testUsers, timestamps...); err != nil {
@ -477,9 +477,9 @@ func TestUserMethods(t *testing.T) {
db := setupTestDB(t) db := setupTestDB(t)
defer db.Close() defer db.Close()
alice, _ := Find(db, 1) // verified, auth 0 alice, _ := Find(db, 1) // verified, auth 0
bob, _ := Find(db, 2) // verified, auth 2 (moderator) bob, _ := Find(db, 2) // verified, auth 2 (moderator)
charlie, _ := Find(db, 3) // unverified, auth 4 (admin) charlie, _ := Find(db, 3) // unverified, auth 4 (admin)
// Test time methods // Test time methods
registeredTime := alice.RegisteredTime() registeredTime := alice.RegisteredTime()
@ -644,7 +644,7 @@ func TestGetEquipmentAndStats(t *testing.T) {
t.Error("Expected non-nil equipment map") t.Error("Expected non-nil equipment map")
} }
weapon, ok := equipment["weapon"].(map[string]interface{}) weapon, ok := equipment["weapon"].(map[string]any)
if !ok { if !ok {
t.Error("Expected weapon to be a map") t.Error("Expected weapon to be a map")
} }
@ -667,4 +667,4 @@ func TestGetEquipmentAndStats(t *testing.T) {
if stats["strength"] != alice.Strength { if stats["strength"] != alice.Strength {
t.Errorf("Expected strength %d, got %d", alice.Strength, stats["strength"]) t.Errorf("Expected strength %d, got %d", alice.Strength, stats["strength"])
} }
} }

13
main.go
View File

@ -1,6 +1,7 @@
package main package main
import ( import (
"flag"
"fmt" "fmt"
"log" "log"
"os" "os"
@ -10,8 +11,11 @@ import (
) )
func main() { func main() {
var port string
flag.StringVar(&port, "p", "3000", "Port to run server on")
if len(os.Args) < 2 { if len(os.Args) < 2 {
startServer() startServer(port)
return return
} }
@ -21,7 +25,8 @@ func main() {
log.Fatalf("Installation failed: %v", err) log.Fatalf("Installation failed: %v", err)
} }
case "serve": case "serve":
startServer() flag.CommandLine.Parse(os.Args[2:])
startServer(port)
default: default:
fmt.Fprintf(os.Stderr, "Unknown command: %s\n", os.Args[1]) fmt.Fprintf(os.Stderr, "Unknown command: %s\n", os.Args[1])
fmt.Fprintln(os.Stderr, "Available commands:") fmt.Fprintln(os.Stderr, "Available commands:")
@ -32,9 +37,9 @@ func main() {
} }
} }
func startServer() { func startServer(port string) {
fmt.Println("Starting Dragon Knight server...") fmt.Println("Starting Dragon Knight server...")
if err := server.Start(); err != nil { if err := server.Start(port); err != nil {
log.Fatalf("Server failed: %v", err) log.Fatalf("Server failed: %v", err)
} }
} }