create news package

This commit is contained in:
Sky Johnson 2025-08-08 23:37:08 -05:00
parent a6b34b7b87
commit 089246dd25
4 changed files with 1143 additions and 0 deletions

85
internal/news/builder.go Normal file
View File

@ -0,0 +1,85 @@
package news
import (
"fmt"
"time"
"dk/internal/database"
"zombiezen.com/go/sqlite"
)
// Builder provides a fluent interface for creating news posts
type Builder struct {
news *News
db *database.DB
}
// NewBuilder creates a new news builder
func NewBuilder(db *database.DB) *Builder {
return &Builder{
news: &News{
db: db,
Posted: time.Now().Unix(), // Default to current time
},
db: db,
}
}
// WithAuthor sets the author ID
func (b *Builder) WithAuthor(authorID int) *Builder {
b.news.Author = authorID
return b
}
// WithContent sets the news content
func (b *Builder) WithContent(content string) *Builder {
b.news.Content = content
return b
}
// WithPosted sets the posted timestamp
func (b *Builder) WithPosted(posted int64) *Builder {
b.news.Posted = posted
return b
}
// WithPostedTime sets the posted timestamp from a time.Time
func (b *Builder) WithPostedTime(t time.Time) *Builder {
b.news.Posted = t.Unix()
return b
}
// Create saves the news post to the database and returns the created news with ID
func (b *Builder) Create() (*News, error) {
// Use a transaction to ensure we can get the ID
var news *News
err := b.db.Transaction(func(tx *database.Tx) error {
query := `INSERT INTO news (author, posted, content)
VALUES (?, ?, ?)`
if err := tx.Exec(query, b.news.Author, b.news.Posted, b.news.Content); err != nil {
return fmt.Errorf("failed to insert news: %w", err)
}
// Get the last insert ID
var id int
err := tx.Query("SELECT last_insert_rowid()", func(stmt *sqlite.Stmt) error {
id = stmt.ColumnInt(0)
return nil
})
if err != nil {
return fmt.Errorf("failed to get insert ID: %w", err)
}
b.news.ID = id
news = b.news
return nil
})
if err != nil {
return nil, err
}
return news, nil
}

349
internal/news/doc.go Normal file
View File

@ -0,0 +1,349 @@
/*
Package news is the active record implementation for news posts in the game.
# Basic Usage
To retrieve a news post by ID:
newsPost, err := news.Find(db, 1)
if err != nil {
log.Fatal(err)
}
fmt.Printf("News: %s (by user %d)\n", newsPost.Content, newsPost.Author)
To get all news posts:
allNews, err := news.All(db)
if err != nil {
log.Fatal(err)
}
for _, post := range allNews {
fmt.Printf("News: %s\n", post.Content)
}
To get recent news posts:
recentNews, err := news.Recent(db, 10)
if err != nil {
log.Fatal(err)
}
To filter news by author:
authorPosts, err := news.ByAuthor(db, userID)
if err != nil {
log.Fatal(err)
}
To get news since a specific time:
yesterday := time.Now().AddDate(0, 0, -1).Unix()
recentNews, err := news.Since(db, yesterday)
if err != nil {
log.Fatal(err)
}
# Creating News with Builder Pattern
The package provides a fluent builder interface for creating new news posts:
newsPost, err := news.NewBuilder(db).
WithAuthor(userID).
WithContent("Welcome to the new update! Many exciting features await.").
WithPostedTime(time.Now()).
Create()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Created news post with ID: %d\n", newsPost.ID)
The builder automatically sets the current time if no posted time is specified:
newsPost, err := news.NewBuilder(db).
WithAuthor(adminID).
WithContent("Server maintenance scheduled for tonight.").
Create() // Uses current timestamp
# Updating News
News posts can be modified and saved back to the database:
newsPost, _ := news.Find(db, 1)
newsPost.Content = "Updated: Server maintenance completed successfully."
err := newsPost.Save()
if err != nil {
log.Fatal(err)
}
# Deleting News
News posts can be removed from the database:
newsPost, _ := news.Find(db, 1)
err := newsPost.Delete()
if err != nil {
log.Fatal(err)
}
# Database Schema
The news table has the following structure:
CREATE TABLE news (
id INTEGER PRIMARY KEY AUTOINCREMENT,
author INTEGER NOT NULL,
posted INTEGER NOT NULL DEFAULT (unixepoch()),
content TEXT NOT NULL
)
Where:
- id: Unique identifier for the news post
- author: User ID of the author who created the post
- posted: Unix timestamp when the post was created
- content: The text content of the news post
# Time-Based Queries
## Recent News
Get the most recent news posts:
// Get 5 most recent posts
latestNews, err := news.Recent(db, 5)
if err != nil {
log.Fatal(err)
}
for _, post := range latestNews {
fmt.Printf("Posted %s: %s\n",
post.PostedTime().Format("2006-01-02"),
post.Preview(50))
}
## News Since Timestamp
Get news posts since a specific time:
// Get news from the last week
weekAgo := time.Now().AddDate(0, 0, -7).Unix()
weeklyNews, err := news.Since(db, weekAgo)
if err != nil {
log.Fatal(err)
}
## News Between Timestamps
Get news posts within a time range:
// Get news from last month
start := time.Now().AddDate(0, -1, 0).Unix()
end := time.Now().Unix()
monthlyNews, err := news.Between(db, start, end)
if err != nil {
log.Fatal(err)
}
# Content Management
## Content Analysis
The package provides utilities for analyzing news content:
wordCount := newsPost.WordCount()
fmt.Printf("Post contains %d words\n", wordCount)
preview := newsPost.Preview(100)
fmt.Printf("Preview: %s\n", preview)
## Author Management
Check authorship and manage posts by author:
if newsPost.IsAuthor(currentUserID) {
fmt.Println("You can edit this post")
}
// Get all posts by a specific author
authorPosts, err := news.ByAuthor(db, authorID)
fmt.Printf("Author has written %d posts\n", len(authorPosts))
# Time Utilities
## Working with Timestamps
The package provides convenient time utilities:
// Get posting time as time.Time
postTime := newsPost.PostedTime()
fmt.Printf("Posted on: %s\n", postTime.Format("January 2, 2006"))
// Set posting time from time.Time
newsPost.SetPostedTime(time.Now().Add(-2 * time.Hour))
// Check if post is recent (within 24 hours)
if newsPost.IsRecent() {
fmt.Println("This is a recent post")
}
// Get age of the post
age := newsPost.Age()
fmt.Printf("Posted %v ago\n", age)
## Time-Based Filtering
Find posts based on recency:
allNews, _ := news.All(db)
recentPosts := make([]*news.News, 0)
for _, post := range allNews {
if post.IsRecent() {
recentPosts = append(recentPosts, post)
}
}
fmt.Printf("Found %d recent posts\n", len(recentPosts))
# Administrative Features
## Moderation
Administrators can manage news posts:
// Get all posts by a user for moderation
suspiciousPosts, err := news.ByAuthor(db, reportedUserID)
for _, post := range suspiciousPosts {
if post.WordCount() < 5 {
fmt.Printf("Short post flagged: %s\n", post.Preview(50))
}
}
## Content Guidelines
Use content analysis for moderation:
if newsPost.WordCount() > 1000 {
fmt.Println("Warning: Very long post")
}
if len(newsPost.Content) < 10 {
fmt.Println("Warning: Very short post")
}
# Display and Formatting
## News Feeds
Display news in chronological order:
recentNews, _ := news.Recent(db, 20)
fmt.Println("=== Latest News ===")
for _, post := range recentNews {
age := post.Age()
var ageStr string
if age < time.Hour {
ageStr = fmt.Sprintf("%d minutes ago", int(age.Minutes()))
} else if age < 24*time.Hour {
ageStr = fmt.Sprintf("%d hours ago", int(age.Hours()))
} else {
ageStr = fmt.Sprintf("%d days ago", int(age.Hours()/24))
}
fmt.Printf("[%s] %s\n", ageStr, post.Preview(80))
}
## Preview Generation
Generate previews for different contexts:
// Short preview for mobile
mobilePreview := newsPost.Preview(30)
// Medium preview for web cards
cardPreview := newsPost.Preview(100)
// Long preview for detailed view
detailPreview := newsPost.Preview(200)
# Performance Considerations
## Query Optimization
All time-based queries are optimized with proper indexing expectations:
// Queries are ordered by posted timestamp for efficient retrieval
latestNews, _ := news.All(db) // ORDER BY posted DESC, id DESC
// Author queries include time ordering
authorPosts, _ := news.ByAuthor(db, userID) // ORDER BY posted DESC, id DESC
## Batch Operations
For processing multiple posts efficiently:
// Process posts in batches
batchSize := 100
allNews, _ := news.All(db)
for i := 0; i < len(allNews); i += batchSize {
end := i + batchSize
if end > len(allNews) {
end = len(allNews)
}
batch := allNews[i:end]
// Process batch...
}
# Error Handling
All functions return appropriate errors for common failure cases:
- News post not found (Find returns error for non-existent IDs)
- Database connection issues
- Invalid operations (e.g., saving/deleting news posts without IDs)
- Time range query errors
# Integration Patterns
## User Integration
Connect with user management:
// Example: Get news by username (assuming user lookup)
user := getUserByName("admin")
adminNews, err := news.ByAuthor(db, user.ID)
## Notification System
Use for activity feeds:
// Get user's activity since last login
lastLogin := getUserLastLogin(userID)
newsSinceLogin, err := news.Since(db, lastLogin)
if len(newsSinceLogin) > 0 {
fmt.Printf("You have %d new posts since your last visit\n", len(newsSinceLogin))
}
## Archive Management
Implement content archiving:
// Archive old posts (older than 1 year)
oneYearAgo := time.Now().AddDate(-1, 0, 0).Unix()
oldPosts, _ := news.Since(db, 0) // Get all posts
for _, post := range oldPosts {
if post.Posted < oneYearAgo {
// Archive or delete old post
post.Delete()
}
}
*/
package news

250
internal/news/news.go Normal file
View File

@ -0,0 +1,250 @@
package news
import (
"fmt"
"time"
"dk/internal/database"
"zombiezen.com/go/sqlite"
)
// News represents a news post in the database
type News struct {
ID int `json:"id"`
Author int `json:"author"`
Posted int64 `json:"posted"`
Content string `json:"content"`
db *database.DB
}
// Find retrieves a news post by ID
func Find(db *database.DB, id int) (*News, error) {
news := &News{db: db}
query := "SELECT id, author, posted, content FROM news WHERE id = ?"
err := db.Query(query, func(stmt *sqlite.Stmt) error {
news.ID = stmt.ColumnInt(0)
news.Author = stmt.ColumnInt(1)
news.Posted = stmt.ColumnInt64(2)
news.Content = stmt.ColumnText(3)
return nil
}, id)
if err != nil {
return nil, fmt.Errorf("failed to find news: %w", err)
}
if news.ID == 0 {
return nil, fmt.Errorf("news with ID %d not found", id)
}
return news, nil
}
// All retrieves all news posts ordered by posted date (newest first)
func All(db *database.DB) ([]*News, error) {
var newsPosts []*News
query := "SELECT id, author, posted, content FROM news ORDER BY posted DESC, id DESC"
err := db.Query(query, func(stmt *sqlite.Stmt) error {
news := &News{
ID: stmt.ColumnInt(0),
Author: stmt.ColumnInt(1),
Posted: stmt.ColumnInt64(2),
Content: stmt.ColumnText(3),
db: db,
}
newsPosts = append(newsPosts, news)
return nil
})
if err != nil {
return nil, fmt.Errorf("failed to retrieve all news: %w", err)
}
return newsPosts, nil
}
// ByAuthor retrieves news posts by a specific author
func ByAuthor(db *database.DB, authorID int) ([]*News, error) {
var newsPosts []*News
query := "SELECT id, author, posted, content FROM news WHERE author = ? ORDER BY posted DESC, id DESC"
err := db.Query(query, func(stmt *sqlite.Stmt) error {
news := &News{
ID: stmt.ColumnInt(0),
Author: stmt.ColumnInt(1),
Posted: stmt.ColumnInt64(2),
Content: stmt.ColumnText(3),
db: db,
}
newsPosts = append(newsPosts, news)
return nil
}, authorID)
if err != nil {
return nil, fmt.Errorf("failed to retrieve news by author: %w", err)
}
return newsPosts, nil
}
// Recent retrieves the most recent news posts (limited by count)
func Recent(db *database.DB, limit int) ([]*News, error) {
var newsPosts []*News
query := "SELECT id, author, posted, content FROM news ORDER BY posted DESC, id DESC LIMIT ?"
err := db.Query(query, func(stmt *sqlite.Stmt) error {
news := &News{
ID: stmt.ColumnInt(0),
Author: stmt.ColumnInt(1),
Posted: stmt.ColumnInt64(2),
Content: stmt.ColumnText(3),
db: db,
}
newsPosts = append(newsPosts, news)
return nil
}, limit)
if err != nil {
return nil, fmt.Errorf("failed to retrieve recent news: %w", err)
}
return newsPosts, nil
}
// Since retrieves news posts since a specific timestamp
func Since(db *database.DB, since int64) ([]*News, error) {
var newsPosts []*News
query := "SELECT id, author, posted, content FROM news WHERE posted >= ? ORDER BY posted DESC, id DESC"
err := db.Query(query, func(stmt *sqlite.Stmt) error {
news := &News{
ID: stmt.ColumnInt(0),
Author: stmt.ColumnInt(1),
Posted: stmt.ColumnInt64(2),
Content: stmt.ColumnText(3),
db: db,
}
newsPosts = append(newsPosts, news)
return nil
}, since)
if err != nil {
return nil, fmt.Errorf("failed to retrieve news since timestamp: %w", err)
}
return newsPosts, nil
}
// Between retrieves news posts between two timestamps (inclusive)
func Between(db *database.DB, start, end int64) ([]*News, error) {
var newsPosts []*News
query := "SELECT id, author, posted, content FROM news WHERE posted >= ? AND posted <= ? ORDER BY posted DESC, id DESC"
err := db.Query(query, func(stmt *sqlite.Stmt) error {
news := &News{
ID: stmt.ColumnInt(0),
Author: stmt.ColumnInt(1),
Posted: stmt.ColumnInt64(2),
Content: stmt.ColumnText(3),
db: db,
}
newsPosts = append(newsPosts, news)
return nil
}, start, end)
if err != nil {
return nil, fmt.Errorf("failed to retrieve news between timestamps: %w", err)
}
return newsPosts, nil
}
// Save updates an existing news post in the database
func (n *News) Save() error {
if n.ID == 0 {
return fmt.Errorf("cannot save news without ID")
}
query := `UPDATE news SET author = ?, posted = ?, content = ? WHERE id = ?`
return n.db.Exec(query, n.Author, n.Posted, n.Content, n.ID)
}
// Delete removes the news post from the database
func (n *News) Delete() error {
if n.ID == 0 {
return fmt.Errorf("cannot delete news without ID")
}
query := "DELETE FROM news WHERE id = ?"
return n.db.Exec(query, n.ID)
}
// PostedTime returns the posted timestamp as a time.Time
func (n *News) PostedTime() time.Time {
return time.Unix(n.Posted, 0)
}
// SetPostedTime sets the posted timestamp from a time.Time
func (n *News) SetPostedTime(t time.Time) {
n.Posted = t.Unix()
}
// IsRecent returns true if the news post was made within the last 24 hours
func (n *News) IsRecent() bool {
return time.Since(n.PostedTime()) < 24*time.Hour
}
// Age returns how long ago the news post was made
func (n *News) Age() time.Duration {
return time.Since(n.PostedTime())
}
// IsAuthor returns true if the given user ID is the author of this news post
func (n *News) IsAuthor(userID int) bool {
return n.Author == userID
}
// Preview returns a truncated version of the content for previews
func (n *News) Preview(maxLength int) string {
if len(n.Content) <= maxLength {
return n.Content
}
if maxLength < 3 {
return n.Content[:maxLength]
}
return n.Content[:maxLength-3] + "..."
}
// WordCount returns the number of words in the content
func (n *News) WordCount() int {
if n.Content == "" {
return 0
}
// Simple word count by splitting on spaces
words := 0
inWord := false
for _, char := range n.Content {
if char == ' ' || char == '\t' || char == '\n' || char == '\r' {
if inWord {
words++
inWord = false
}
} else {
inWord = true
}
}
if inWord {
words++
}
return words
}

459
internal/news/news_test.go Normal file
View File

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