Skip to content

Commit 5b2e1b1

Browse files
committed
feat(download): implement workflow enrichment function and add tests for HandleDownloadRequest
1 parent 9f6ca95 commit 5b2e1b1

2 files changed

Lines changed: 157 additions & 2 deletions

File tree

internal/download/workflow.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/alvarorichard/Goanime/internal/api/providers/metadata"
1212
"github.com/alvarorichard/Goanime/internal/appflow"
1313
"github.com/alvarorichard/Goanime/internal/downloader"
14+
"github.com/alvarorichard/Goanime/internal/models"
1415
"github.com/alvarorichard/Goanime/internal/player"
1516
"github.com/alvarorichard/Goanime/internal/util"
1617
)
@@ -19,6 +20,12 @@ import (
1920
// Tests may override it to avoid spawning a real TUI search.
2021
var workflowSearchFn = appflow.SearchAnimeWithRetry
2122

23+
// workflowEnrichFn resolves the AniList season mapping for the found anime.
24+
// Tests may override it to avoid the real AniList request.
25+
var workflowEnrichFn = func(ctx context.Context, anime *models.Anime) ([]metadata.SeasonMapping, error) {
26+
return metadata.NewEnricher().EnrichAnime(ctx, anime)
27+
}
28+
2229
// HandleDownloadRequest processes a download request from command line
2330
func HandleDownloadRequest(request *util.DownloadRequest) error {
2431
util.Info("Starting enhanced download mode...")
@@ -53,8 +60,7 @@ func HandleDownloadRequest(request *util.DownloadRequest) error {
5360
MalID: anime.MalID,
5461
})
5562

56-
enricher := metadata.NewEnricher()
57-
seasonMap, _ := enricher.EnrichAnime(context.Background(), anime)
63+
seasonMap, _ := workflowEnrichFn(context.Background(), anime)
5864
player.SetSeasonMap(seasonMap)
5965

6066
player.SetMediaMeta(&util.MediaMeta{
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package download
2+
3+
import (
4+
"context"
5+
"errors"
6+
"testing"
7+
8+
"github.com/alvarorichard/Goanime/internal/api/providers/metadata"
9+
"github.com/alvarorichard/Goanime/internal/models"
10+
"github.com/alvarorichard/Goanime/internal/player"
11+
"github.com/alvarorichard/Goanime/internal/util"
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
14+
)
15+
16+
// enrichRecorder records how HandleDownloadRequest drives the enrichment seam.
17+
type enrichRecorder struct {
18+
called bool
19+
anime *models.Anime
20+
err error
21+
}
22+
23+
// stubWorkflow points both workflow seams at offline fakes and turns on strict
24+
// source resolution so providers.FetchEpisodes fails fast instead of guessing
25+
// a live source. The anime URL is loopback, which SafeGet rejects before
26+
// dialing (SSRF guard), so the legacy episode fetch also fails fast — every
27+
// path is deterministic and touches no real network.
28+
// These tests mutate package seams and process env, so none are parallel.
29+
func stubWorkflow(t *testing.T, anime *models.Anime) *enrichRecorder {
30+
t.Helper()
31+
t.Setenv("GOANIME_STRICT_SOURCE", "1")
32+
33+
rec := &enrichRecorder{}
34+
prevSearch, prevEnrich := workflowSearchFn, workflowEnrichFn
35+
prevMeta := player.GetMediaMeta()
36+
workflowSearchFn = func(_ string) (*models.Anime, error) { return anime, nil }
37+
workflowEnrichFn = func(_ context.Context, a *models.Anime) ([]metadata.SeasonMapping, error) {
38+
rec.called = true
39+
rec.anime = a
40+
return []metadata.SeasonMapping{}, rec.err
41+
}
42+
t.Cleanup(func() {
43+
workflowSearchFn = prevSearch
44+
workflowEnrichFn = prevEnrich
45+
player.SetMediaMeta(prevMeta)
46+
})
47+
return rec
48+
}
49+
50+
// offlineAnime returns an anime whose URL is loopback (blocked by SafeGet) and
51+
// whose source is unrecognizable (errors under GOANIME_STRICT_SOURCE).
52+
func offlineAnime() *models.Anime {
53+
return &models.Anime{
54+
Name: "Workflow Test Anime",
55+
URL: "http://127.0.0.1:9/blocked",
56+
MediaType: models.MediaTypeAnime,
57+
AnilistID: 42,
58+
MalID: 7,
59+
}
60+
}
61+
62+
func TestHandleDownloadRequest_SingleEpisode_LegacyFetchFails(t *testing.T) {
63+
anime := offlineAnime()
64+
rec := stubWorkflow(t, anime)
65+
66+
req := &util.DownloadRequest{AnimeName: "test", EpisodeNum: 1}
67+
err := HandleDownloadRequest(req)
68+
69+
require.Error(t, err)
70+
assert.Contains(t, err.Error(), "failed to fetch episodes")
71+
assert.True(t, rec.called, "enrichment must run before the episode fetch")
72+
assert.Same(t, anime, rec.anime)
73+
}
74+
75+
func TestHandleDownloadRequest_AllEpisodes_FallbackCascade(t *testing.T) {
76+
// Cascade: providers.FetchEpisodes fails (strict source) → batch download
77+
// is skipped → legacy fetch fails (loopback blocked) → error surfaces.
78+
stubWorkflow(t, offlineAnime())
79+
80+
req := &util.DownloadRequest{AnimeName: "test", IsAll: true}
81+
err := HandleDownloadRequest(req)
82+
83+
require.Error(t, err)
84+
assert.Contains(t, err.Error(), "failed to fetch episodes")
85+
}
86+
87+
func TestHandleDownloadRequest_Range_FallbackCascade(t *testing.T) {
88+
stubWorkflow(t, offlineAnime())
89+
90+
req := &util.DownloadRequest{
91+
AnimeName: "test",
92+
IsRange: true,
93+
StartEpisode: 1,
94+
EndEpisode: 3,
95+
}
96+
err := HandleDownloadRequest(req)
97+
98+
require.Error(t, err)
99+
assert.Contains(t, err.Error(), "failed to fetch episodes")
100+
}
101+
102+
func TestHandleDownloadRequest_SmartRange_RequiresAllAnimeSource(t *testing.T) {
103+
// AllAnimeSmart set but the anime is not from AllAnime: the smart branch
104+
// must be skipped and the normal range cascade taken instead.
105+
stubWorkflow(t, offlineAnime())
106+
107+
req := &util.DownloadRequest{
108+
AnimeName: "test",
109+
IsRange: true,
110+
StartEpisode: 2,
111+
EndEpisode: 4,
112+
AllAnimeSmart: true,
113+
Source: "animefire",
114+
}
115+
err := HandleDownloadRequest(req)
116+
117+
require.Error(t, err)
118+
assert.Contains(t, err.Error(), "failed to fetch episodes",
119+
"non-AllAnime smart request must fall through to the normal range path")
120+
}
121+
122+
func TestHandleDownloadRequest_EnrichErrorIsNonFatal(t *testing.T) {
123+
anime := offlineAnime()
124+
rec := stubWorkflow(t, anime)
125+
rec.err = errors.New("anilist down")
126+
127+
req := &util.DownloadRequest{AnimeName: "test", EpisodeNum: 1}
128+
err := HandleDownloadRequest(req)
129+
130+
// The enrichment error is swallowed; the flow proceeds to the episode
131+
// fetch and fails there, not on enrichment.
132+
require.Error(t, err)
133+
assert.Contains(t, err.Error(), "failed to fetch episodes")
134+
assert.True(t, rec.called)
135+
}
136+
137+
func TestHandleDownloadRequest_MediaMetaPropagated(t *testing.T) {
138+
anime := offlineAnime()
139+
stubWorkflow(t, anime)
140+
141+
req := &util.DownloadRequest{AnimeName: "test", EpisodeNum: 1, SeasonNum: 3, Quality: "720p"}
142+
_ = HandleDownloadRequest(req)
143+
144+
meta := player.GetMediaMeta()
145+
require.NotNil(t, meta)
146+
assert.Equal(t, 42, meta.AnilistID)
147+
assert.Equal(t, 7, meta.MalID)
148+
assert.Equal(t, anime.OfficialTitle(), meta.OfficialTitle)
149+
}

0 commit comments

Comments
 (0)