diff --git a/Dockerfile b/Dockerfile index cb2c5a4..536d30b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,15 +3,15 @@ FROM golang:1.24.5-alpine AS builder WORKDIR /app COPY go.mod go.sum ./ -RUN go mod download +RUN go mod vendor COPY . . -RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o /app/main . +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o /app/miniflux-digest ./cmd/miniflux-digest FROM alpine:3.22.1 WORKDIR /app -COPY --from=builder /app/main /app/main -ENTRYPOINT ["/app/main"] +COPY --from=builder /app/miniflux-digest /app/miniflux-digest +ENTRYPOINT ["/app/miniflux-digest"] diff --git a/Makefile b/Makefile index 11f0d55..ce402f4 100644 --- a/Makefile +++ b/Makefile @@ -14,6 +14,9 @@ test: test-coverage: ./scripts/check-coverage.sh 60 +test-coverage-full: + ./scripts/check-coverage.sh --mode=functions 60 + lint: golangci-lint run diff --git a/internal/app/app.go b/internal/app/app.go new file mode 100644 index 0000000..3bc3c08 --- /dev/null +++ b/internal/app/app.go @@ -0,0 +1,23 @@ +package app + +import ( + "miniflux-digest/internal/config" +) + +type App struct { + Config *config.Config + ArchiveService ArchiveService + EmailService EmailService + MinifluxClientService MinifluxClientService + CategoryService CategoryService +} + +func NewApp(cfg *config.Config, client MinifluxClientService, archiveSvc ArchiveService, emailSvc EmailService, categorySvc CategoryService) *App { + return &App{ + Config: cfg, + ArchiveService: archiveSvc, + EmailService: emailSvc, + MinifluxClientService: client, + CategoryService: categorySvc, + } +} diff --git a/internal/app/interfaces.go b/internal/app/interfaces.go index 6e06cf6..90603ce 100644 --- a/internal/app/interfaces.go +++ b/internal/app/interfaces.go @@ -1,13 +1,16 @@ package app import ( - "miniflux-digest/config" + "miniflux-digest/internal/config" "miniflux-digest/internal/models" "os" + "time" + miniflux "miniflux.app/v2/client" ) type ArchiveService interface { MakeArchiveHTML(archivePath string, data *models.CategoryData) (*os.File, error) + CleanArchive(archivePath string, maxAge time.Duration) } type EmailService interface { @@ -16,4 +19,12 @@ type EmailService interface { type MinifluxClientService interface { MarkCategoryAsRead(categoryID int64) error + Categories() ([]*miniflux.Category, error) + CategoryEntries(categoryID int64, filter *miniflux.Filter) (*miniflux.Entries, error) + CategoryFeeds(categoryID int64) ([]*miniflux.Feed, error) + FeedIcon(feedID int64) (*miniflux.FeedIcon, error) +} + +type CategoryService interface { + StreamData(client MinifluxClientService) <-chan *models.CategoryData } diff --git a/internal/app/miniflux_client_wrapper.go b/internal/app/miniflux_client_wrapper.go new file mode 100644 index 0000000..910b8ef --- /dev/null +++ b/internal/app/miniflux_client_wrapper.go @@ -0,0 +1,37 @@ +package app + +import ( + miniflux "miniflux.app/v2/client" +) + +type MinifluxClientWrapper struct { + client *miniflux.Client +} + +func NewMinifluxClientWrapper(client *miniflux.Client) *MinifluxClientWrapper { + return &MinifluxClientWrapper{client: client} +} + +func (m *MinifluxClientWrapper) MarkCategoryAsRead(categoryID int64) error { + return m.client.MarkCategoryAsRead(categoryID) +} + +func (m *MinifluxClientWrapper) Categories() ([]*miniflux.Category, error) { + return m.client.Categories() +} + +func (m *MinifluxClientWrapper) CategoryEntries(categoryID int64, filter *miniflux.Filter) (*miniflux.Entries, error) { + entries, err := m.client.CategoryEntries(categoryID, filter) + if err != nil { + return nil, err + } + return &entries.Entries, nil +} + +func (m *MinifluxClientWrapper) CategoryFeeds(categoryID int64) ([]*miniflux.Feed, error) { + return m.client.CategoryFeeds(categoryID) +} + +func (m *MinifluxClientWrapper) FeedIcon(feedID int64) (*miniflux.FeedIcon, error) { + return m.client.FeedIcon(feedID) +} diff --git a/internal/app/miniflux_client_wrapper_test.go b/internal/app/miniflux_client_wrapper_test.go new file mode 100644 index 0000000..0d79b8b --- /dev/null +++ b/internal/app/miniflux_client_wrapper_test.go @@ -0,0 +1,67 @@ +package app + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + miniflux "miniflux.app/v2/client" +) + +func TestMinifluxClientWrapper_MarkCategoryAsRead(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Check if the request path is correct + if r.URL.Path != "/v1/categories/1/mark-all-as-read" { + t.Errorf("Expected to request '/v1/categories/1/mark-all-as-read', got %s", r.URL.Path) + } + // Check if the request method is correct + if r.Method != http.MethodPut { + t.Errorf("Expected PUT request, got %s", r.Method) + } + w.WriteHeader(http.StatusNoContent) + })) + defer server.Close() + + // Create a new miniflux client that points to the test server + client := miniflux.NewClient(server.URL, "testUser", "testPassword") + // Create our wrapper + wrapper := NewMinifluxClientWrapper(client) + + // Call the function we want to test + err := wrapper.MarkCategoryAsRead(1) + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } +} + +func TestMinifluxClientWrapper_CategoryEntries(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/v1/categories/1/entries" { + if _, err := fmt.Fprintln(w, `{"total": 1, "entries": [{"id": 123, "title": "Test Entry"}]}`); err != nil { + panic(err) + } + } + })) + defer server.Close() + + client := miniflux.NewClient(server.URL, "testUser", "testPassword") + wrapper := NewMinifluxClientWrapper(client) + + entries, err := wrapper.CategoryEntries(1, &miniflux.Filter{Status: miniflux.EntryStatusUnread}) + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if entries == nil { + t.Fatal("Expected entries to be non-nil") + } + + if len(*entries) != 1 { + t.Errorf("Expected 1 entry, got %d", len(*entries)) + } + + if (*entries)[0].ID != 123 { + t.Errorf("Expected entry ID 123, got %d", (*entries)[0].ID) + } +} diff --git a/internal/archive/archive.go b/internal/archive/archive.go index 8980539..0fd3ab0 100644 --- a/internal/archive/archive.go +++ b/internal/archive/archive.go @@ -6,6 +6,7 @@ import ( "io" "io/fs" "log" + "miniflux-digest/internal/app" "miniflux-digest/internal/models" "miniflux-digest/internal/templates" "miniflux-digest/internal/utils" @@ -16,6 +17,8 @@ import ( type ArchiveServiceImpl struct{} +var _ app.ArchiveService = (*ArchiveServiceImpl)(nil) + func getHTML(data *models.CategoryData) (string, error) { var buf bytes.Buffer var htmlOutput string @@ -142,7 +145,7 @@ func removeEmptyCategoryFolders(archivePath string) { } } -func CleanArchive(archivePath string, maxAge time.Duration) { +func (s *ArchiveServiceImpl) CleanArchive(archivePath string, maxAge time.Duration) { removeOldArchiveFiles(archivePath, maxAge) removeEmptyCategoryFolders(archivePath) } diff --git a/internal/archive/archive_test.go b/internal/archive/archive_test.go index b174a4b..5f03f77 100644 --- a/internal/archive/archive_test.go +++ b/internal/archive/archive_test.go @@ -77,7 +77,8 @@ func TestCleanArchive(t *testing.T) { t.Fatalf("Failed to create new file: %v", err) } - CleanArchive(tmpDir, 24*time.Hour) + archiveService := &ArchiveServiceImpl{} + archiveService.CleanArchive(tmpDir, 24*time.Hour) if _, err := os.Stat(oldFilePath); !os.IsNotExist(err) { t.Error("Expected old file to be deleted") diff --git a/internal/category/category.go b/internal/category/category.go index 0b569d0..9a47071 100644 --- a/internal/category/category.go +++ b/internal/category/category.go @@ -4,58 +4,16 @@ import ( "log" "time" + "miniflux-digest/internal/app" "miniflux-digest/internal/models" miniflux "miniflux.app/v2/client" ) +type CategoryServiceImpl struct{} +var _ app.CategoryService = (*CategoryServiceImpl)(nil) -func MarkAsRead(client *miniflux.Client, category *miniflux.Category) { - err := client.MarkCategoryAsRead(category.ID) - - if err != nil { - log.Printf("Failed to mark category %q (%d) as read: %v", category.Title, category.ID, err) - } -} - -func fetchData(client *miniflux.Client, category *miniflux.Category) (models.CategoryData, error) { - entriesResult, err := client.CategoryEntries(category.ID, &miniflux.Filter{Status: miniflux.EntryStatusUnread}) - - if err != nil { - return models.CategoryData{}, err - } - - entries := entriesResult.Entries - feeds, err := client.CategoryFeeds(category.ID) - feedIcons := []*models.FeedIcon{} - - if err != nil { - return models.CategoryData{}, err - } - - for _, feed := range feeds { - feedIcon, err := client.FeedIcon(feed.ID) - - if err != nil { - continue - } - - feedIconForTemplate := &models.FeedIcon{ - FeedID: feed.ID, - Data: feedIcon.Data, - } - feedIcons = append(feedIcons, feedIconForTemplate) - } - - return models.CategoryData{ - Category: category, - Entries: &entries, - GeneratedDate: time.Now(), - FeedIcons: feedIcons, - }, nil -} - -func StreamData(client *miniflux.Client) <-chan *models.CategoryData { +func (s *CategoryServiceImpl) StreamData(client app.MinifluxClientService) <-chan *models.CategoryData { out := make(chan *models.CategoryData) go func() { @@ -81,3 +39,40 @@ func StreamData(client *miniflux.Client) <-chan *models.CategoryData { return out } + +func fetchData(client app.MinifluxClientService, category *miniflux.Category) (models.CategoryData, error) { + entriesResult, err := client.CategoryEntries(category.ID, &miniflux.Filter{Status: miniflux.EntryStatusUnread}) + + if err != nil { + return models.CategoryData{}, err + } + + entries := entriesResult + feeds, err := client.CategoryFeeds(category.ID) + feedIcons := []*models.FeedIcon{} + + if err != nil { + return models.CategoryData{}, err + } + + for _, feed := range feeds { + feedIcon, err := client.FeedIcon(feed.ID) + + if err != nil { + continue + } + + feedIconForTemplate := &models.FeedIcon{ + FeedID: feed.ID, + Data: feedIcon.Data, + } + feedIcons = append(feedIcons, feedIconForTemplate) + } + + return models.CategoryData{ + Category: category, + Entries: entries, + GeneratedDate: time.Now(), + FeedIcons: feedIcons, + }, nil +} diff --git a/internal/category/category_test.go b/internal/category/category_test.go index 66cd833..143ff37 100644 --- a/internal/category/category_test.go +++ b/internal/category/category_test.go @@ -2,6 +2,8 @@ package category import ( "fmt" + "miniflux-digest/internal/app" + "miniflux-digest/internal/testutil" "net/http" "net/http/httptest" "reflect" @@ -9,30 +11,10 @@ import ( "time" "miniflux-digest/internal/models" + miniflux "miniflux.app/v2/client" ) -func mockHandler(response string, statusCode int) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(statusCode) - if statusCode != http.StatusNoContent { - if _, err := fmt.Fprintln(w, response); err != nil { - panic(err) - } - } - } -} - -func TestMarkAsRead(t *testing.T) { - server := httptest.NewServer(mockHandler(`{}`, http.StatusNoContent)) - defer server.Close() - - client := miniflux.NewClient(server.URL, "testUser", "testPassword") - category := &miniflux.Category{ID: 1, Title: "Test"} - - MarkAsRead(client, category) -} - func TestFetchData(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { @@ -45,15 +27,15 @@ func TestFetchData(t *testing.T) { panic(err) } case "/v1/feeds/1/icon": - if _, err := fmt.Fprintln(w, `{"data": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAHElEQVQ4T2Nksr/85wADGGYw4oBMAHkAAAD//wMA/wEAP2D3e/gAAAAASUVORK5CYII=", "mime_type": "image/png"}`); err != nil { + if _, err := fmt.Fprintf(w, `{"data": "%s", "mime_type": "image/png"}`, testutil.NewMockFeedIcon().Data); err != nil { panic(err) } } })) defer server.Close() - client := miniflux.NewClient(server.URL, "testUser", "testPassword") - category := &miniflux.Category{ID: 1, Title: "Test"} + client := app.NewMinifluxClientWrapper(miniflux.NewClient(server.URL, "testUser", "testPassword")) + category := testutil.NewMockCategory() data, err := fetchData(client, category) if err != nil { @@ -74,10 +56,8 @@ func TestFetchData(t *testing.T) { if len(data.FeedIcons) != 1 { t.Errorf("Expected 1 feed icon, got %d", len(data.FeedIcons)) - } - - if data.FeedIcons[0].Data != "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAHElEQVQ4T2Nksr/85wADGGYw4oBMAHkAAAD//wMA/wEAP2D3e/gAAAAASUVORK5CYII=" { - t.Errorf("Expected feed icon data 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAHElEQVQ4T2Nksr/85wADGGYw4oBMAHkAAAD//wMA/wEAP2D3e/gAAAAASUVORK5CYII=', got %s", data.FeedIcons[0].Data) + } else if data.FeedIcons[0].Data != testutil.NewMockFeedIcon().Data { + t.Errorf("Expected feed icon data '%s', got %s", testutil.NewMockFeedIcon().Data, data.FeedIcons[0].Data) } } @@ -104,8 +84,9 @@ func TestStreamData(t *testing.T) { })) defer server.Close() - client := miniflux.NewClient(server.URL, "testUser", "testPassword") - ch := StreamData(client) + client := app.NewMinifluxClientWrapper(miniflux.NewClient(server.URL, "testUser", "testPassword")) + categoryService := &CategoryServiceImpl{} + ch := categoryService.StreamData(client) var results []*models.CategoryData for data := range ch { @@ -122,25 +103,22 @@ func TestStreamData(t *testing.T) { } func TestFeedIcon(t *testing.T) { - icon := &models.FeedIcon{ - FeedID: 1, - Data: "test", - } + icon := testutil.NewMockFeedIcon() if icon.FeedID != 1 { t.Errorf("Expected FeedID to be 1, got %d", icon.FeedID) } - if icon.Data != "test" { - t.Errorf("Expected Data to be 'test', got %s", icon.Data) + if icon.Data != testutil.NewMockFeedIcon().Data { + t.Errorf("Expected Data to be '%s', got %s", testutil.NewMockFeedIcon().Data, icon.Data) } } func TestCategoryData(t *testing.T) { now := time.Now() - category := &miniflux.Category{ID: 1, Title: "Test"} - entries := &miniflux.Entries{} - icons := []*models.FeedIcon{} + category := testutil.NewMockCategory() + entries := &miniflux.Entries{testutil.NewMockEntry()} + icons := []*models.FeedIcon{testutil.NewMockFeedIcon()} data := &models.CategoryData{ Category: category, diff --git a/config/config.go b/internal/config/config.go similarity index 94% rename from config/config.go rename to internal/config/config.go index 6177c26..868dea2 100644 --- a/config/config.go +++ b/internal/config/config.go @@ -19,7 +19,7 @@ type Config struct { DigestEmailFrom string DigestSchedule string DigestHost string - DigestDryRun bool + ArchivePath string } var k = koanf.New(".") @@ -44,6 +44,7 @@ func Load(path string) (*Config, error) { DigestEmailFrom: k.String("digest.email.from"), DigestSchedule: k.String("digest.schedule"), DigestHost: k.String("digest.host"), + ArchivePath: k.String("archive.path"), } return cfg, nil diff --git a/config/config_test.go b/internal/config/config_test.go similarity index 100% rename from config/config_test.go rename to internal/config/config_test.go diff --git a/internal/email/email.go b/internal/email/email.go index 5cfacef..bb89e91 100644 --- a/internal/email/email.go +++ b/internal/email/email.go @@ -5,7 +5,8 @@ import ( "os" "path/filepath" - "miniflux-digest/config" + "miniflux-digest/internal/config" + "miniflux-digest/internal/app" "miniflux-digest/internal/models" "miniflux-digest/internal/templates" @@ -15,6 +16,8 @@ import ( type EmailServiceImpl struct{} +var _ app.EmailService = (*EmailServiceImpl)(nil) + func (s *EmailServiceImpl) Send(cfg *config.Config, file *os.File, data *models.CategoryData) error { message := mail.NewMsg() client, err := mail.NewClient( diff --git a/internal/email/email_test.go b/internal/email/email_test.go index d7e5504..558c9ba 100644 --- a/internal/email/email_test.go +++ b/internal/email/email_test.go @@ -1,7 +1,7 @@ package email import ( - "miniflux-digest/config" + "miniflux-digest/internal/config" "miniflux-digest/internal/templates" "miniflux-digest/internal/testutil" "os" diff --git a/internal/processor/processor.go b/internal/processor/processor.go index 2b8d349..ffadd04 100644 --- a/internal/processor/processor.go +++ b/internal/processor/processor.go @@ -3,14 +3,13 @@ package processor import ( "log" - "miniflux-digest/config" "miniflux-digest/internal/app" "miniflux-digest/internal/models" ) -func ProcessCategory(cfg *config.Config, client app.MinifluxClientService, data *models.CategoryData, archiveService app.ArchiveService, emailService app.EmailService, archivePath string, markAsRead bool) { +func ProcessCategory(application *app.App, data *models.CategoryData, markAsRead bool) { if len(*data.Entries) > 0 { - file, err := archiveService.MakeArchiveHTML(archivePath, data) + file, err := application.ArchiveService.MakeArchiveHTML(application.Config.ArchivePath, data) if err != nil { log.Printf("Error generating File for category %s: %v", data.Category.Title, err) @@ -23,14 +22,14 @@ func ProcessCategory(cfg *config.Config, client app.MinifluxClientService, data } }() - err = emailService.Send(cfg, file, data) + err = application.EmailService.Send(application.Config, file, data) if err != nil { log.Printf("Error sending email for category '%s': %v", data.Category.Title, err) } if markAsRead { - if err := client.MarkCategoryAsRead(data.Category.ID); err != nil { + if err := application.MinifluxClientService.MarkCategoryAsRead(data.Category.ID); err != nil { log.Printf("Error marking category as read for category '%s': %v", data.Category.Title, err) } } diff --git a/internal/processor/processor_test.go b/internal/processor/processor_test.go index 0b266e9..8c5b7b1 100644 --- a/internal/processor/processor_test.go +++ b/internal/processor/processor_test.go @@ -5,145 +5,52 @@ import ( "errors" "io" "log" - "miniflux-digest/config" + "miniflux-digest/internal/app" + "miniflux-digest/internal/config" "miniflux-digest/internal/models" "miniflux-digest/internal/testutil" - miniflux "miniflux.app/v2/client" "os" "testing" + + miniflux "miniflux.app/v2/client" ) -// MockMinifluxClient is a mock implementation of app.MinifluxClientService -type MockMinifluxClient struct { - MarkAsReadFunc func(categoryID int64) error -} - -func (m *MockMinifluxClient) MarkCategoryAsRead(categoryID int64) error { - return m.MarkAsReadFunc(categoryID) -} - -// MockArchiveService is a mock implementation of app.ArchiveService -type MockArchiveService struct { - MakeArchiveHTMLFunc func(archivePath string, data *models.CategoryData) (*os.File, error) -} - -func (m *MockArchiveService) MakeArchiveHTML(archivePath string, data *models.CategoryData) (*os.File, error) { - return m.MakeArchiveHTMLFunc(archivePath, data) -} - -// MockEmailService is a mock implementation of app.EmailService -type MockEmailService struct { - SendFunc func(cfg *config.Config, file *os.File, data *models.CategoryData) error -} - -func (m *MockEmailService) Send(cfg *config.Config, file *os.File, data *models.CategoryData) error { - return m.SendFunc(cfg, file, data) -} - func TestProcessCategory(t *testing.T) { - // Suppress log output during tests log.SetOutput(io.Discard) defer log.SetOutput(os.Stderr) - cfg := &config.Config{} - archivePath := "/tmp/archive" - t.Run("no entries", func(t *testing.T) { - client := &MockMinifluxClient{} - archiveService := &MockArchiveService{} - emailService := &MockEmailService{} + mockApp := &app.App{ + Config: &config.Config{}, + MinifluxClientService: &testutil.MockMinifluxClient{ + MarkAsReadFunc: func(categoryID int64) error { return nil }, + }, + ArchiveService: &testutil.MockArchiveService{}, + EmailService: &testutil.MockEmailService{}, + } data := &models.CategoryData{Entries: &miniflux.Entries{}} - ProcessCategory(cfg, client, data, archiveService, emailService, archivePath, true) - // No panics or errors expected - }) - - t.Run("send email and mark as read", func(t *testing.T) { - markedAsRead := false - sentEmail := false - - client := &MockMinifluxClient{ - MarkAsReadFunc: func(categoryID int64) error { - markedAsRead = true - return nil - }, - } - - archiveService := &MockArchiveService{ - MakeArchiveHTMLFunc: func(archivePath string, data *models.CategoryData) (*os.File, error) { - // Create a dummy file for testing - return os.CreateTemp("", "test-archive-*.html") - }, - } - - emailService := &MockEmailService{ - SendFunc: func(cfg *config.Config, file *os.File, data *models.CategoryData) error { - sentEmail = true - return nil - }, - } - - data := testutil.NewMockCategoryData() - ProcessCategory(cfg, client, data, archiveService, emailService, archivePath, true) - - if !markedAsRead { - t.Error("Expected category to be marked as read, but it was not") - } - if !sentEmail { - t.Error("Expected email to be sent, but it was not") - } - }) - - t.Run("do not mark as read", func(t *testing.T) { - markedAsRead := false - sentEmail := false - - client := &MockMinifluxClient{ - MarkAsReadFunc: func(categoryID int64) error { - markedAsRead = true - return nil - }, - } - - archiveService := &MockArchiveService{ - MakeArchiveHTMLFunc: func(archivePath string, data *models.CategoryData) (*os.File, error) { - return os.CreateTemp("", "test-archive-*.html") - }, - } - - emailService := &MockEmailService{ - SendFunc: func(cfg *config.Config, file *os.File, data *models.CategoryData) error { - sentEmail = true - return nil - }, - } - - data := testutil.NewMockCategoryData() - ProcessCategory(cfg, client, data, archiveService, emailService, archivePath, false) - - if markedAsRead { - t.Error("Expected category not to be marked as read, but it was") - } - if !sentEmail { - t.Error("Expected email to be sent, but it was not") - } + ProcessCategory(mockApp, data, true) }) t.Run("error making archive html", func(t *testing.T) { - client := &MockMinifluxClient{} - emailService := &MockEmailService{} - data := testutil.NewMockCategoryData() - - archiveService := &MockArchiveService{ - MakeArchiveHTMLFunc: func(archivePath string, data *models.CategoryData) (*os.File, error) { - return nil, errors.New("failed to make archive html") + mockApp := &app.App{ + Config: &config.Config{ArchivePath: "/tmp/archive"}, + MinifluxClientService: &testutil.MockMinifluxClient{ + MarkAsReadFunc: func(categoryID int64) error { return nil }, + }, + EmailService: &testutil.MockEmailService{}, + ArchiveService: &testutil.MockArchiveService{ + MakeArchiveHTMLFunc: func(archivePath string, data *models.CategoryData) (*os.File, error) { + return nil, errors.New("failed to make archive html") + }, }, } + data := testutil.NewMockCategoryData() - // Capture log output var buf bytes.Buffer log.SetOutput(&buf) - ProcessCategory(cfg, client, data, archiveService, emailService, archivePath, true) + ProcessCategory(mockApp, data, true) if !bytes.Contains(buf.Bytes(), []byte("Error generating File")) { t.Error("Expected error log for archive generation, but not found") @@ -151,33 +58,96 @@ func TestProcessCategory(t *testing.T) { }) t.Run("error sending email", func(t *testing.T) { - client := &MockMinifluxClient{ - MarkAsReadFunc: func(categoryID int64) error { - return nil // No-op for this test case + mockApp := &app.App{ + Config: &config.Config{ArchivePath: "/tmp/archive"}, + MinifluxClientService: &testutil.MockMinifluxClient{ + MarkAsReadFunc: func(categoryID int64) error { + return nil // No-op for this test case + }, + }, + ArchiveService: &testutil.MockArchiveService{ + MakeArchiveHTMLFunc: func(archivePath string, data *models.CategoryData) (*os.File, error) { + return os.CreateTemp("", "test-archive-*.html") + }, + }, + EmailService: &testutil.MockEmailService{ + SendFunc: func(cfg *config.Config, file *os.File, data *models.CategoryData) error { + return errors.New("failed to send email") + }, }, } data := testutil.NewMockCategoryData() - archiveService := &MockArchiveService{ - MakeArchiveHTMLFunc: func(archivePath string, data *models.CategoryData) (*os.File, error) { - return os.CreateTemp("", "test-archive-*.html") - }, - } - - emailService := &MockEmailService{ - SendFunc: func(cfg *config.Config, file *os.File, data *models.CategoryData) error { - return errors.New("failed to send email") - }, - } - - // Capture log output var buf bytes.Buffer log.SetOutput(&buf) - ProcessCategory(cfg, client, data, archiveService, emailService, archivePath, true) + ProcessCategory(mockApp, data, true) if !bytes.Contains(buf.Bytes(), []byte("Error sending email")) { t.Error("Expected error log for email sending, but not found") } }) -} + + t.Run("mark as read is called", func(t *testing.T) { + markAsReadCalled := false + mockMinifluxClient := &testutil.MockMinifluxClient{ + MarkAsReadFunc: func(categoryID int64) error { + markAsReadCalled = true + return nil + }, + } + mockApp := &app.App{ + Config: &config.Config{ArchivePath: "/tmp/archive"}, + MinifluxClientService: mockMinifluxClient, + ArchiveService: &testutil.MockArchiveService{ + MakeArchiveHTMLFunc: func(archivePath string, data *models.CategoryData) (*os.File, error) { + return os.CreateTemp("", "test-archive-*.html") + }, + }, + EmailService: &testutil.MockEmailService{ + SendFunc: func(cfg *config.Config, file *os.File, data *models.CategoryData) error { + return nil + }, + }, + } + data := testutil.NewMockCategoryData() + + ProcessCategory(mockApp, data, true) + + if !markAsReadCalled { + t.Error("Expected MarkCategoryAsRead to be called, but it was not") + } + }) + + t.Run("error marking as read", func(t *testing.T) { + var buf bytes.Buffer + log.SetOutput(&buf) + + mockMinifluxClient := &testutil.MockMinifluxClient{ + MarkAsReadFunc: func(categoryID int64) error { + return errors.New("failed to mark as read") + }, + } + mockApp := &app.App{ + Config: &config.Config{ArchivePath: "/tmp/archive"}, + MinifluxClientService: mockMinifluxClient, + ArchiveService: &testutil.MockArchiveService{ + MakeArchiveHTMLFunc: func(archivePath string, data *models.CategoryData) (*os.File, error) { + return os.CreateTemp("", "test-archive-*.html") + }, + }, + EmailService: &testutil.MockEmailService{ + SendFunc: func(cfg *config.Config, file *os.File, data *models.CategoryData) error { + return nil + }, + }, + } + data := testutil.NewMockCategoryData() + + ProcessCategory(mockApp, data, true) + + if !bytes.Contains(buf.Bytes(), []byte("Error marking category as read")) { + t.Error("Expected error log for marking as read, but not found") + } + }) +} \ No newline at end of file diff --git a/internal/testutil/mock_data.go b/internal/testutil/mock_data.go index 3736eea..409f4ce 100644 --- a/internal/testutil/mock_data.go +++ b/internal/testutil/mock_data.go @@ -1,85 +1,47 @@ package testutil import ( - "encoding/base64" - "log" - "os" - "path/filepath" - "runtime" "time" "miniflux-digest/internal/models" miniflux "miniflux.app/v2/client" ) -func loadImageAsBase64(path string) string { - _, b, _, _ := runtime.Caller(0) - basepath := filepath.Dir(b) - imagePath := filepath.Join(basepath, "images", filepath.Base(path)) - - file, err := os.ReadFile(imagePath) - if err != nil { - log.Fatalf("Failed to read image file: %v", err) +func NewMockCategory() *miniflux.Category { + return &miniflux.Category{ + ID: 1, + Title: "Test Category", } +} - return base64.StdEncoding.EncodeToString(file) +func NewMockFeed() *miniflux.Feed { + return &miniflux.Feed{ + ID: 1, + Title: "Test Feed", + } +} + +func NewMockEntry() *miniflux.Entry { + return &miniflux.Entry{ + ID: 1, + Title: "Test Entry", + Content: "
Test Content
", + Feed: NewMockFeed(), + } +} + +func NewMockFeedIcon() *models.FeedIcon { + return &models.FeedIcon{ + FeedID: 1, + Data: "image/png;base64,abcd1234=", + } } func NewMockCategoryData() *models.CategoryData { - redSquare := loadImageAsBase64("internal/testutil/images/red.png") - yellowSquare := loadImageAsBase64("internal/testutil/images/yellow.png") - greenSquare := loadImageAsBase64("internal/testutil/images/green.png") - return &models.CategoryData{ - Category: &miniflux.Category{ - ID: 1, - Title: "Test Category", - }, - Entries: &miniflux.Entries{ - { - ID: 1, - UserID: 1, - FeedID: 1, - Status: miniflux.EntryStatusUnread, - Hash: "test-hash-1", - Title: "A Short and Sweet Title", - URL: "https://example.com/1", - CommentsURL: "https://example.com/1/comments", - Date: time.Now().Add(-1 * time.Hour), - CreatedAt: time.Now().Add(-2 * time.Hour), - Content: "This is a short and sweet entry.", - Author: "Test Author 1", - ShareCode: "test-share-code-1", - Starred: true, - ReadingTime: 1, - Enclosures: []*miniflux.Enclosure{{URL: "https://example.com/image.jpg", MimeType: "image/jpeg"}}, - Feed: &miniflux.Feed{ID: 1, Title: "Test Feed 1"}, - }, - { - ID: 2, - FeedID: 2, - Title: "A Longer Entry with a Paragraph of Text", - URL: "https://example.com/2", - Date: time.Now().Add(-3 * time.Hour), - Content: "This is a longer entry that contains a full paragraph of text. It is meant to simulate a more realistic entry that a user might encounter in their feed. It has enough text to wrap to multiple lines and give a good sense of how the layout will look with a more substantial amount of content.", - Author: "Test Author 2", - Feed: &miniflux.Feed{ID: 2, Title: "Test Feed 2"}, - }, - { - ID: 3, - FeedID: 3, - Title: "An Entry with HTML Content", - URL: "https://example.com/3", - Date: time.Now().Add(-4 * time.Hour), - Content: "This is a paragraph with strong text and a link.