refactor: overhaul test suite and application architecture #15
No files matched your search
+4
-4
@@ -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"]
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
File renamed without changes.
@@ -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(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package email
|
||||
|
||||
import (
|
||||
"miniflux-digest/config"
|
||||
"miniflux-digest/internal/config"
|
||||
"miniflux-digest/internal/templates"
|
||||
"miniflux-digest/internal/testutil"
|
||||
"os"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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: "<p>Test Content</p>",
|
||||
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: "<h1>This is a heading</h1><p>This is a paragraph with <strong>strong</strong> text and a <a href=\"https://example.com\">link</a>.</p><ul><li>This is a list item</li><li>This is another list item</li></ul>",
|
||||
Feed: &miniflux.Feed{ID: 3, Title: "Test Feed 3"},
|
||||
},
|
||||
},
|
||||
Category: NewMockCategory(),
|
||||
Entries: &miniflux.Entries{NewMockEntry()},
|
||||
GeneratedDate: time.Date(2025, 7, 21, 12, 0, 0, 0, time.UTC),
|
||||
FeedIcons: []*models.FeedIcon{
|
||||
{FeedID: 1, Data: "image/png;base64," + redSquare},
|
||||
{FeedID: 2, Data: "image/png;base64," + yellowSquare},
|
||||
{FeedID: 3, Data: "image/png;base64," + greenSquare},
|
||||
},
|
||||
FeedIcons: []*models.FeedIcon{NewMockFeedIcon()},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"miniflux-digest/internal/app"
|
||||
"miniflux-digest/internal/config"
|
||||
"miniflux-digest/internal/models"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
type MockMinifluxClient struct {
|
||||
app.MinifluxClientService
|
||||
MarkAsReadFunc func(categoryID int64) error
|
||||
}
|
||||
|
||||
func (m *MockMinifluxClient) MarkCategoryAsRead(categoryID int64) error {
|
||||
return m.MarkAsReadFunc(categoryID)
|
||||
}
|
||||
|
||||
type MockArchiveService struct {
|
||||
app.ArchiveService
|
||||
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)
|
||||
}
|
||||
|
||||
func (m *MockArchiveService) CleanArchive(archivePath string, maxAge time.Duration) {}
|
||||
|
||||
type MockEmailService struct {
|
||||
app.EmailService
|
||||
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)
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/go-co-op/gocron/v2"
|
||||
miniflux "miniflux.app/v2/client"
|
||||
|
||||
"miniflux-digest/config"
|
||||
"miniflux-digest/internal/archive"
|
||||
"miniflux-digest/internal/category"
|
||||
"miniflux-digest/internal/email"
|
||||
"miniflux-digest/internal/processor"
|
||||
)
|
||||
|
||||
|
||||
|
||||
func checkAndSendDigests(cfg *config.Config, archivePath string) {
|
||||
|
||||
client := miniflux.NewClient(cfg.MinifluxHost, cfg.MinifluxApiToken)
|
||||
|
||||
for data := range category.StreamData(client) {
|
||||
processor.ProcessCategory(cfg, client, data, &archive.ArchiveServiceImpl{}, &email.EmailServiceImpl{}, archivePath, true)
|
||||
}
|
||||
}
|
||||
|
||||
func registerDigestsJob(cfg *config.Config, scheduler gocron.Scheduler, archivePath string) {
|
||||
_, err := scheduler.NewJob(gocron.CronJob(cfg.DigestSchedule, true), gocron.NewTask(func() {
|
||||
checkAndSendDigests(cfg, archivePath)
|
||||
}))
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Error creating job: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func registerArchiveCleanupJob(scheduler gocron.Scheduler, archivePath string) {
|
||||
_, err := scheduler.NewJob(gocron.DurationJob(time.Hour*24), gocron.NewTask(func() {
|
||||
archive.CleanArchive(archivePath, time.Hour*24*21)
|
||||
}))
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Error creating job: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg, err := config.Load("./config.yaml")
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Error loading configuration: %v", err)
|
||||
}
|
||||
|
||||
archivePath := "./web/miniflux-archive"
|
||||
|
||||
scheduler, err := gocron.NewScheduler()
|
||||
|
||||
defer func() {
|
||||
if err := scheduler.Shutdown(); err != nil {
|
||||
log.Printf("Error stopping scheduler: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Error creating scheduler: %v", err)
|
||||
}
|
||||
|
||||
registerDigestsJob(cfg, scheduler, archivePath)
|
||||
registerArchiveCleanupJob(scheduler, archivePath)
|
||||
|
||||
scheduler.Start()
|
||||
|
||||
select {}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"miniflux-digest/config"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/go-co-op/gocron/v2"
|
||||
)
|
||||
|
||||
func TestCheckAndSendDigestsWithDryRun(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/v1/categories":
|
||||
if _, err := fmt.Fprintln(w, `[{"id": 1, "title": "Test Category"}]`); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
case "/v1/categories/1/entries":
|
||||
if _, err := fmt.Fprintln(w, `{"entries": [{"id": 1, "title": "Test Entry"}], "total": 1}`); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
case "/v1/categories/1/feeds":
|
||||
if _, err := fmt.Fprintln(w, `[{"id": 1, "title": "Test Feed"}]`); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
case "/v1/feeds/1/icon":
|
||||
if _, err := fmt.Fprintln(w, `{"data": "icon-data", "mime_type": "image/png"}`); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cfg := &config.Config{
|
||||
MinifluxHost: server.URL,
|
||||
MinifluxApiToken: "test-token",
|
||||
DigestDryRun: true,
|
||||
}
|
||||
checkAndSendDigests(cfg, t.TempDir())
|
||||
}
|
||||
|
||||
func TestJobRegistration(t *testing.T) {
|
||||
cfg := &config.Config{DigestSchedule: "@daily"}
|
||||
scheduler, err := gocron.NewScheduler()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create scheduler: %v", err)
|
||||
}
|
||||
|
||||
registerDigestsJob(cfg, scheduler, t.TempDir())
|
||||
registerArchiveCleanupJob(scheduler, t.TempDir())
|
||||
|
||||
jobs := scheduler.Jobs()
|
||||
if len(jobs) != 2 {
|
||||
t.Errorf("Expected 2 jobs to be registered, got %d", len(jobs))
|
||||
}
|
||||
|
||||
if err := scheduler.Shutdown(); err != nil {
|
||||
t.Fatalf("Failed to shutdown scheduler: %v", err)
|
||||
}
|
||||
}
|
||||
+25
-12
@@ -2,26 +2,39 @@
|
||||
|
||||
set -eu
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: $0 <coverage_threshold>"
|
||||
exit 1
|
||||
# Default to package view, but allow function view
|
||||
MODE="packages"
|
||||
if [ "${1:-}" = "--mode=functions" ]; then
|
||||
MODE="functions"
|
||||
fi
|
||||
|
||||
COVERAGE_THRESHOLD=$1
|
||||
# Always get the coverage threshold from the last argument
|
||||
COVERAGE_THRESHOLD=$(echo "$@" | awk '{print $NF}')
|
||||
|
||||
# --- Test Execution ---
|
||||
EXCLUDED_PACKAGES="miniflux-digest/internal/testutil miniflux-digest/scripts"
|
||||
COVERAGE_PACKAGES_COMMA=$(./scripts/list-packages.sh $EXCLUDED_PACKAGES)
|
||||
COVERAGE_PACKAGES_SPACE=$(echo $COVERAGE_PACKAGES_COMMA | tr ',' ' ')
|
||||
|
||||
COVERAGE_PACKAGES=$(./scripts/list-packages.sh $EXCLUDED_PACKAGES)
|
||||
# Run tests once to generate the coverage.out file
|
||||
go test -mod=vendor -coverprofile=coverage.out -coverpkg=$COVERAGE_PACKAGES_COMMA $COVERAGE_PACKAGES_SPACE > /dev/null 2>&1
|
||||
|
||||
go test -mod=vendor -coverprofile=coverage.out -coverpkg=$COVERAGE_PACKAGES ./... ./cmd/miniflux-digest
|
||||
# --- Reporting ---
|
||||
echo "Test Coverage Report"
|
||||
echo "--------------------"
|
||||
|
||||
if [ "$MODE" = "functions" ]; then
|
||||
# Detailed, function-by-function view
|
||||
go tool cover -func=coverage.out | grep -v -E "$(echo $EXCLUDED_PACKAGES | tr ' ' '|')"
|
||||
else
|
||||
# Clean, package-by-package view
|
||||
go test -mod=vendor -coverpkg=$COVERAGE_PACKAGES_COMMA $COVERAGE_PACKAGES_SPACE | sed 's/ in .*//'
|
||||
fi
|
||||
|
||||
# --- Threshold Check ---
|
||||
COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//')
|
||||
|
||||
if ! echo "$COVERAGE" | grep -qE '^[0-9]+(\.[0-9]+)?$'; then
|
||||
echo "Error: Could not determine test coverage."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "\n--------------------"
|
||||
echo "Total test coverage: $COVERAGE%"
|
||||
|
||||
if awk -v cov="$COVERAGE" -v min="$COVERAGE_THRESHOLD" 'BEGIN {exit (cov < min)}'; then
|
||||
@@ -29,4 +42,4 @@ if awk -v cov="$COVERAGE" -v min="$COVERAGE_THRESHOLD" 'BEGIN {exit (cov < min)}
|
||||
else
|
||||
echo "Error: Test coverage is below the ${COVERAGE_THRESHOLD}% threshold."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
@@ -3,7 +3,8 @@ package main
|
||||
import (
|
||||
"log"
|
||||
|
||||
"miniflux-digest/config"
|
||||
"miniflux-digest/internal/app"
|
||||
"miniflux-digest/internal/config"
|
||||
"miniflux-digest/internal/archive"
|
||||
"miniflux-digest/internal/category"
|
||||
"miniflux-digest/internal/email"
|
||||
@@ -17,10 +18,17 @@ func main() {
|
||||
log.Fatalf("Failed to load config: %v", err)
|
||||
}
|
||||
|
||||
client := miniflux.NewClient(cfg.MinifluxHost, cfg.MinifluxApiToken)
|
||||
minifluxClient := miniflux.NewClient(cfg.MinifluxHost, cfg.MinifluxApiToken)
|
||||
clientWrapper := app.NewMinifluxClientWrapper(minifluxClient)
|
||||
|
||||
for data := range category.StreamData(client) {
|
||||
processor.ProcessCategory(cfg, client, data, &archive.ArchiveServiceImpl{}, &email.EmailServiceImpl{}, "./web/miniflux-archive", false)
|
||||
archiveSvc := &archive.ArchiveServiceImpl{}
|
||||
emailSvc := &email.EmailServiceImpl{}
|
||||
categorySvc := &category.CategoryServiceImpl{}
|
||||
|
||||
application := app.NewApp(cfg, clientWrapper, archiveSvc, emailSvc, categorySvc)
|
||||
|
||||
for data := range application.CategoryService.StreamData(application.MinifluxClientService) {
|
||||
processor.ProcessCategory(application, data, false)
|
||||
break
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user