aboutsummaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorJeff Halter <868228+jhalter@users.noreply.github.com>2025-07-04 22:07:15 -0700
committerJeff Halter <868228+jhalter@users.noreply.github.com>2025-07-04 22:07:15 -0700
commit1f3e5ec139022c20be8ca61b467b96ae7e78a36a (patch)
treed2833215f524ce4e29500b07ea55eb1c2d7df87b /internal
parent9e19aa65d1cf6740c1280279b0b70f9423cffbd7 (diff)
Add comprehensive test coverage for News and improve error handling
Diffstat (limited to 'internal')
-rw-r--r--internal/mobius/news.go6
-rw-r--r--internal/mobius/news_test.go500
2 files changed, 505 insertions, 1 deletions
diff --git a/internal/mobius/news.go b/internal/mobius/news.go
index 13a728a..c63c7f7 100644
--- a/internal/mobius/news.go
+++ b/internal/mobius/news.go
@@ -66,6 +66,7 @@ func (f *FlatNews) Write(p []byte) (int, error) {
f.mu.Lock()
defer f.mu.Unlock()
+ // Prepend the new post to the existing news posts.
f.data = slices.Concat(p, f.data)
tempFilePath := f.filePath + ".tmp"
@@ -79,10 +80,13 @@ func (f *FlatNews) Write(p []byte) (int, error) {
return 0, fmt.Errorf("rename temporary file to final file: %v", err)
}
- return len(p), os.WriteFile(f.filePath, f.data, 0644)
+ return len(p), nil
}
func (f *FlatNews) Seek(offset int64, _ int) (int64, error) {
+ f.mu.Lock()
+ defer f.mu.Unlock()
+
f.readOffset = int(offset)
return 0, nil
diff --git a/internal/mobius/news_test.go b/internal/mobius/news_test.go
new file mode 100644
index 0000000..b5d5ccd
--- /dev/null
+++ b/internal/mobius/news_test.go
@@ -0,0 +1,500 @@
+package mobius
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+ "testing"
+)
+
+func TestNewFlatNews(t *testing.T) {
+ tests := []struct {
+ name string
+ setupFile func(string) error
+ filePath string
+ wantErr bool
+ wantErrMsg string
+ }{
+ {
+ name: "valid file with content",
+ setupFile: func(path string) error {
+ return os.WriteFile(path, []byte("test news content\nwith newlines"), 0644)
+ },
+ filePath: "test_news.txt",
+ wantErr: false,
+ },
+ {
+ name: "valid empty file",
+ setupFile: func(path string) error {
+ return os.WriteFile(path, []byte(""), 0644)
+ },
+ filePath: "empty_news.txt",
+ wantErr: false,
+ },
+ {
+ name: "nonexistent file",
+ setupFile: func(path string) error { return nil },
+ filePath: "nonexistent.txt",
+ wantErr: true,
+ wantErrMsg: "reload:",
+ },
+ {
+ name: "file with mixed line endings",
+ setupFile: func(path string) error {
+ return os.WriteFile(path, []byte("line1\nline2\r\nline3\r"), 0644)
+ },
+ filePath: "mixed_endings.txt",
+ wantErr: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tempDir := t.TempDir()
+ fullPath := filepath.Join(tempDir, tt.filePath)
+
+ if err := tt.setupFile(fullPath); err != nil {
+ t.Fatalf("Failed to setup test file: %v", err)
+ }
+
+ flatNews, err := NewFlatNews(fullPath)
+
+ if tt.wantErr {
+ if err == nil {
+ t.Error("Expected error but got none")
+ } else if tt.wantErrMsg != "" && !containsSubstring(err.Error(), tt.wantErrMsg) {
+ t.Errorf("Expected error to contain %q, got %q", tt.wantErrMsg, err.Error())
+ }
+ return
+ }
+
+ if err != nil {
+ t.Errorf("Unexpected error: %v", err)
+ return
+ }
+
+ if flatNews == nil {
+ t.Error("Expected FlatNews instance but got nil")
+ return
+ }
+
+ if flatNews.filePath != fullPath {
+ t.Errorf("Expected filePath %q, got %q", fullPath, flatNews.filePath)
+ }
+ })
+ }
+}
+
+func TestFlatNews_Reload(t *testing.T) {
+ tests := []struct {
+ name string
+ initialData string
+ newData string
+ expectData string
+ wantErr bool
+ deleteFile bool
+ }{
+ {
+ name: "reload with new content",
+ initialData: "initial content",
+ newData: "new content\nwith newlines",
+ expectData: "new content\rwith newlines",
+ wantErr: false,
+ },
+ {
+ name: "reload with empty content",
+ initialData: "some content",
+ newData: "",
+ expectData: "",
+ wantErr: false,
+ },
+ {
+ name: "reload with mixed line endings",
+ initialData: "old",
+ newData: "line1\nline2\r\nline3\r",
+ expectData: "line1\rline2\r\rline3\r",
+ wantErr: false,
+ },
+ {
+ name: "reload after file deletion",
+ initialData: "content",
+ newData: "",
+ expectData: "",
+ wantErr: true,
+ deleteFile: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tempDir := t.TempDir()
+ filePath := filepath.Join(tempDir, "test.txt")
+
+ if err := os.WriteFile(filePath, []byte(tt.initialData), 0644); err != nil {
+ t.Fatalf("Failed to create initial file: %v", err)
+ }
+
+ flatNews, err := NewFlatNews(filePath)
+ if err != nil {
+ t.Fatalf("Failed to create FlatNews: %v", err)
+ }
+
+ if tt.deleteFile {
+ if err := os.Remove(filePath); err != nil {
+ t.Fatalf("Failed to delete file: %v", err)
+ }
+ } else {
+ if err := os.WriteFile(filePath, []byte(tt.newData), 0644); err != nil {
+ t.Fatalf("Failed to write new data: %v", err)
+ }
+ }
+
+ err = flatNews.Reload()
+
+ if tt.wantErr {
+ if err == nil {
+ t.Error("Expected error but got none")
+ }
+ return
+ }
+
+ if err != nil {
+ t.Errorf("Unexpected error: %v", err)
+ return
+ }
+
+ if string(flatNews.data) != tt.expectData {
+ t.Errorf("Expected data %q, got %q", tt.expectData, string(flatNews.data))
+ }
+ })
+ }
+}
+
+func TestFlatNews_Read(t *testing.T) {
+ tests := []struct {
+ name string
+ fileContent string
+ bufferSize int
+ expectedReads []readResult
+ }{
+ {
+ name: "read all at once",
+ fileContent: "test content\nwith newlines",
+ bufferSize: 100,
+ expectedReads: []readResult{
+ {data: "test content\rwith newlines", n: 26, err: nil},
+ {data: "", n: 0, err: io.EOF},
+ },
+ },
+ {
+ name: "read in chunks",
+ fileContent: "hello world",
+ bufferSize: 5,
+ expectedReads: []readResult{
+ {data: "hello", n: 5, err: nil},
+ {data: " worl", n: 5, err: nil},
+ {data: "d", n: 1, err: nil},
+ {data: "", n: 0, err: io.EOF},
+ },
+ },
+ {
+ name: "read empty file",
+ fileContent: "",
+ bufferSize: 10,
+ expectedReads: []readResult{
+ {data: "", n: 0, err: io.EOF},
+ },
+ },
+ {
+ name: "small buffer large content",
+ fileContent: "abcdefghij",
+ bufferSize: 3,
+ expectedReads: []readResult{
+ {data: "abc", n: 3, err: nil},
+ {data: "def", n: 3, err: nil},
+ {data: "ghi", n: 3, err: nil},
+ {data: "j", n: 1, err: nil},
+ {data: "", n: 0, err: io.EOF},
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tempDir := t.TempDir()
+ filePath := filepath.Join(tempDir, "test.txt")
+
+ if err := os.WriteFile(filePath, []byte(tt.fileContent), 0644); err != nil {
+ t.Fatalf("Failed to create test file: %v", err)
+ }
+
+ flatNews, err := NewFlatNews(filePath)
+ if err != nil {
+ t.Fatalf("Failed to create FlatNews: %v", err)
+ }
+
+ for i, expected := range tt.expectedReads {
+ buf := make([]byte, tt.bufferSize)
+ n, err := flatNews.Read(buf)
+
+ if err != expected.err {
+ t.Errorf("Read %d: expected error %v, got %v", i, expected.err, err)
+ }
+
+ if n != expected.n {
+ t.Errorf("Read %d: expected n %d, got %d", i, expected.n, n)
+ }
+
+ actualData := string(buf[:n])
+ if actualData != expected.data {
+ t.Errorf("Read %d: expected data %q, got %q", i, expected.data, actualData)
+ }
+ }
+ })
+ }
+}
+
+func TestFlatNews_Write(t *testing.T) {
+ tests := []struct {
+ name string
+ initialData string
+ writeData string
+ expectedData string
+ wantErr bool
+ }{
+ {
+ name: "write to empty file",
+ initialData: "",
+ writeData: "new content",
+ expectedData: "new content",
+ wantErr: false,
+ },
+ {
+ name: "prepend to existing content",
+ initialData: "existing",
+ writeData: "new ",
+ expectedData: "new existing",
+ wantErr: false,
+ },
+ {
+ name: "write empty data",
+ initialData: "content",
+ writeData: "",
+ expectedData: "content",
+ wantErr: false,
+ },
+ {
+ name: "write binary data",
+ initialData: "text",
+ writeData: "\x00\x01\x02",
+ expectedData: "\x00\x01\x02text",
+ wantErr: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tempDir := t.TempDir()
+ filePath := filepath.Join(tempDir, "test.txt")
+
+ if err := os.WriteFile(filePath, []byte(tt.initialData), 0644); err != nil {
+ t.Fatalf("Failed to create initial file: %v", err)
+ }
+
+ flatNews, err := NewFlatNews(filePath)
+ if err != nil {
+ t.Fatalf("Failed to create FlatNews: %v", err)
+ }
+
+ n, err := flatNews.Write([]byte(tt.writeData))
+
+ if tt.wantErr {
+ if err == nil {
+ t.Error("Expected error but got none")
+ }
+ return
+ }
+
+ if err != nil {
+ t.Errorf("Unexpected error: %v", err)
+ return
+ }
+
+ if n != len(tt.writeData) {
+ t.Errorf("Expected n %d, got %d", len(tt.writeData), n)
+ }
+
+ if string(flatNews.data) != tt.expectedData {
+ t.Errorf("Expected data %q, got %q", tt.expectedData, string(flatNews.data))
+ }
+
+ fileData, err := os.ReadFile(filePath)
+ if err != nil {
+ t.Errorf("Failed to read file: %v", err)
+ return
+ }
+
+ if string(fileData) != tt.expectedData {
+ t.Errorf("Expected file data %q, got %q", tt.expectedData, string(fileData))
+ }
+ })
+ }
+}
+
+func TestFlatNews_Seek(t *testing.T) {
+ tests := []struct {
+ name string
+ fileContent string
+ offset int64
+ whence int
+ expectOffset int64
+ expectErr bool
+ }{
+ {
+ name: "seek to beginning",
+ fileContent: "test content",
+ offset: 0,
+ whence: 0,
+ expectOffset: 0,
+ expectErr: false,
+ },
+ {
+ name: "seek to middle",
+ fileContent: "test content",
+ offset: 5,
+ whence: 0,
+ expectOffset: 0,
+ expectErr: false,
+ },
+ {
+ name: "seek beyond end",
+ fileContent: "test",
+ offset: 10,
+ whence: 0,
+ expectOffset: 0,
+ expectErr: false,
+ },
+ {
+ name: "negative offset",
+ fileContent: "test",
+ offset: -5,
+ whence: 0,
+ expectOffset: 0,
+ expectErr: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tempDir := t.TempDir()
+ filePath := filepath.Join(tempDir, "test.txt")
+
+ if err := os.WriteFile(filePath, []byte(tt.fileContent), 0644); err != nil {
+ t.Fatalf("Failed to create test file: %v", err)
+ }
+
+ flatNews, err := NewFlatNews(filePath)
+ if err != nil {
+ t.Fatalf("Failed to create FlatNews: %v", err)
+ }
+
+ offset, err := flatNews.Seek(tt.offset, tt.whence)
+
+ if tt.expectErr {
+ if err == nil {
+ t.Error("Expected error but got none")
+ }
+ return
+ }
+
+ if err != nil {
+ t.Errorf("Unexpected error: %v", err)
+ return
+ }
+
+ if offset != tt.expectOffset {
+ t.Errorf("Expected offset %d, got %d", tt.expectOffset, offset)
+ }
+
+ expectedReadOffset := int(tt.offset)
+ if flatNews.readOffset != expectedReadOffset {
+ t.Errorf("Expected readOffset %d, got %d", expectedReadOffset, flatNews.readOffset)
+ }
+ })
+ }
+}
+
+func TestFlatNews_ConcurrentOperations(t *testing.T) {
+ tempDir := t.TempDir()
+ filePath := filepath.Join(tempDir, "concurrent_test.txt")
+
+ if err := os.WriteFile(filePath, []byte("initial content"), 0644); err != nil {
+ t.Fatalf("Failed to create test file: %v", err)
+ }
+
+ flatNews, err := NewFlatNews(filePath)
+ if err != nil {
+ t.Fatalf("Failed to create FlatNews: %v", err)
+ }
+
+ var wg sync.WaitGroup
+ errors := make(chan error, 10)
+
+ for i := 0; i < 5; i++ {
+ wg.Add(1)
+ go func(id int) {
+ defer wg.Done()
+
+ buf := make([]byte, 10)
+ _, err := flatNews.Read(buf)
+ if err != nil && err != io.EOF {
+ errors <- fmt.Errorf("read goroutine %d: %w", id, err)
+ }
+ }(i)
+ }
+
+ for i := 0; i < 3; i++ {
+ wg.Add(1)
+ go func(id int) {
+ defer wg.Done()
+
+ if err := flatNews.Reload(); err != nil {
+ errors <- fmt.Errorf("reload goroutine %d: %w", id, err)
+ }
+ }(i)
+ }
+
+ for i := 0; i < 2; i++ {
+ wg.Add(1)
+ go func(id int) {
+ defer wg.Done()
+
+ data := fmt.Sprintf("data%d", id)
+ if _, err := flatNews.Write([]byte(data)); err != nil {
+ errors <- fmt.Errorf("write goroutine %d: %w", id, err)
+ }
+ }(i)
+ }
+
+ wg.Wait()
+ close(errors)
+
+ for err := range errors {
+ t.Errorf("Concurrent operation error: %v", err)
+ }
+}
+
+type readResult struct {
+ data string
+ n int
+ err error
+}
+
+func containsSubstring(s, substr string) bool {
+ return len(s) >= len(substr) &&
+ (len(substr) == 0 ||
+ strings.Contains(s, substr))
+} \ No newline at end of file