diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-10 09:10:01 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-10 09:10:01 +0300 |
| commit | 93366c7a8603a64b410521dd19d967c0cd4d7e84 (patch) | |
| tree | d9d2439921d02c07aa12e72a790ef3273c3384a8 /internal/service | |
| parent | 297a2f4e2f0aba6f139736030a7845e0d91e5856 (diff) | |
internal/service: fix ListEpisodes global pagination by using cross-feed SQL query
Previously ListEpisodes requested per-feed episodes with the full limit
from every feed, concatenated them, and then sliced the resulting slice
in memory. This silently skipped episodes from deeper pages because the
per-feed DB limit never returned them.
Fix: Add a new repository method ListEpisodesByFeedIDsWithStatus that
takes a slice of feed IDs and applies LIMIT/OFFSET globally in a single
SQL query (IN (...)). The service builds the feed ID list and delegates
pagination to the database instead of emulating it in memory.
Files changed:
- internal/repository/podcast.go: add ListEpisodesByFeedIDsWithStatus
- internal/repository/podcast_repo.go: extend PodcastRepo interface
- internal/repository/mock.go: add mock implementation
- internal/service/podcast.go: replace per-feed loop with new method
- internal/service/podcast_test.go: add tests for ListEpisodes
- internal/repository/podcast_test.go: add integration tests for new repo method
- internal/api/handlers_test.go: add mockPingStore method
Diffstat (limited to 'internal/service')
| -rw-r--r-- | internal/service/podcast.go | 19 | ||||
| -rw-r--r-- | internal/service/podcast_test.go | 76 |
2 files changed, 80 insertions, 15 deletions
diff --git a/internal/service/podcast.go b/internal/service/podcast.go index 9c9e90d..1d4f16b 100644 --- a/internal/service/podcast.go +++ b/internal/service/podcast.go @@ -420,22 +420,11 @@ func (s *podcastService) ListEpisodes(ctx context.Context, setID, userID int64, return nil, ErrNotFound } - all := make([]model.PodcastEpisodeWithStatus, 0) - for _, feed := range feeds { - episodes, err := s.store.ListEpisodesWithStatus(ctx, userID, feed.ID, limit, 0) - if err != nil { - return nil, err - } - all = append(all, episodes...) - } - if offset >= len(all) { - return []model.PodcastEpisodeWithStatus{}, nil - } - end := len(all) - if limit > 0 && offset+limit < end { - end = offset + limit + feedIDs := make([]int64, len(feeds)) + for i, f := range feeds { + feedIDs[i] = f.ID } - return all[offset:end], nil + return s.store.ListEpisodesByFeedIDsWithStatus(ctx, userID, feedIDs, limit, offset) } func (s *podcastService) DownloadEpisode(ctx context.Context, episodeID, userID int64) (*model.Media, error) { diff --git a/internal/service/podcast_test.go b/internal/service/podcast_test.go index 3fb94f9..f630ee0 100644 --- a/internal/service/podcast_test.go +++ b/internal/service/podcast_test.go @@ -914,3 +914,79 @@ func TestPodcastService_CheckFeeds_FeedError_Continues(t *testing.T) { } mu.Unlock() } + +func TestPodcastService_ListEpisodes_GlobalPagination(t *testing.T) { + ctx := context.Background() + svc, store := setupPodcastService(t) + + store.SetPermissionRepo = repository.MockSetPermissionRepo{ + GetPermissionFunc: func(ctx context.Context, setID, userID int64) (*model.SetPermission, error) { + return nil, nil + }, + } + store.UserRepo = repository.MockUserRepo{ + GetUserByIDFunc: func(ctx context.Context, id int64) (*model.User, error) { + return &model.User{ID: id, IsAdmin: true}, nil + }, + } + + store.PodcastRepo = repository.MockPodcastRepo{ + ListFeedsBySetIDFunc: func(ctx context.Context, setID int64) ([]model.PodcastFeed, error) { + return []model.PodcastFeed{ + {ID: 1, SetID: setID, Title: "Feed A"}, + {ID: 2, SetID: setID, Title: "Feed B"}, + }, nil + }, + ListEpisodesByFeedIDsWithStatusFunc: func(ctx context.Context, userID int64, feedIDs []int64, limit, offset int) ([]model.PodcastEpisodeWithStatus, error) { + // Verify service passes both feed IDs and the original limit/offset. + if len(feedIDs) != 2 { + t.Errorf("expected 2 feedIDs, got %d", len(feedIDs)) + } + if limit != 3 { + t.Errorf("expected limit 3, got %d", limit) + } + if offset != 5 { + t.Errorf("expected offset 5, got %d", offset) + } + return []model.PodcastEpisodeWithStatus{ + {PodcastEpisode: model.PodcastEpisode{ID: 1, Title: "Ep 1"}}, + {PodcastEpisode: model.PodcastEpisode{ID: 2, Title: "Ep 2"}}, + {PodcastEpisode: model.PodcastEpisode{ID: 3, Title: "Ep 3"}}, + }, nil + }, + } + + result, err := svc.ListEpisodes(ctx, 42, 1, 3, 5) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(result) != 3 { + t.Fatalf("expected 3 episodes, got %d", len(result)) + } +} + +func TestPodcastService_ListEpisodes_NoFeeds(t *testing.T) { + ctx := context.Background() + svc, store := setupPodcastService(t) + + store.SetPermissionRepo = repository.MockSetPermissionRepo{ + GetPermissionFunc: func(ctx context.Context, setID, userID int64) (*model.SetPermission, error) { + return nil, nil + }, + } + store.UserRepo = repository.MockUserRepo{ + GetUserByIDFunc: func(ctx context.Context, id int64) (*model.User, error) { + return &model.User{ID: id, IsAdmin: true}, nil + }, + } + store.PodcastRepo = repository.MockPodcastRepo{ + ListFeedsBySetIDFunc: func(ctx context.Context, setID int64) ([]model.PodcastFeed, error) { + return []model.PodcastFeed{}, nil + }, + } + + _, err := svc.ListEpisodes(ctx, 42, 1, 10, 0) + if !errors.Is(err, ErrNotFound) { + t.Fatalf("expected ErrNotFound, got %v", err) + } +} |
