274 lines
6.6 KiB
Go
274 lines
6.6 KiB
Go
package anime
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"mal/integrations/jikan"
|
|
"mal/internal/domain"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
type stubEpisodeService struct {
|
|
episodes domain.CanonicalEpisodeList
|
|
err error
|
|
called int
|
|
forceRefresh bool
|
|
}
|
|
|
|
func (s *stubEpisodeService) GetCanonicalEpisodes(ctx context.Context, anime domain.Anime, forceRefresh bool) (domain.CanonicalEpisodeList, error) {
|
|
s.called++
|
|
s.forceRefresh = forceRefresh
|
|
if s.err != nil {
|
|
return domain.CanonicalEpisodeList{}, s.err
|
|
}
|
|
return s.episodes, nil
|
|
}
|
|
|
|
func (s *stubEpisodeService) RefreshTrackedDue(ctx context.Context, limit int) error {
|
|
return nil
|
|
}
|
|
|
|
type releasedCountTest struct {
|
|
name string
|
|
anime domain.Anime
|
|
now time.Time
|
|
want int
|
|
}
|
|
|
|
var releasedCountTests = []releasedCountTest{
|
|
{
|
|
name: "weekly airing count",
|
|
anime: domain.Anime{Anime: jikan.Anime{
|
|
Airing: true,
|
|
Episodes: 24,
|
|
Aired: jikan.Aired{From: "2026-04-04T15:00:00+00:00"},
|
|
}},
|
|
now: time.Date(2026, time.June, 13, 15, 0, 0, 0, time.UTC),
|
|
want: 11,
|
|
},
|
|
{
|
|
name: "before first release",
|
|
anime: domain.Anime{Anime: jikan.Anime{
|
|
Airing: true,
|
|
Aired: jikan.Aired{From: "2026-04-04T15:00:00+00:00"},
|
|
}},
|
|
now: time.Date(2026, time.April, 4, 14, 59, 0, 0, time.UTC),
|
|
want: 0,
|
|
},
|
|
{
|
|
name: "first release counts as one",
|
|
anime: domain.Anime{Anime: jikan.Anime{
|
|
Airing: true,
|
|
Aired: jikan.Aired{From: "2026-04-04T15:00:00+00:00"},
|
|
}},
|
|
now: time.Date(2026, time.April, 4, 15, 0, 0, 0, time.UTC),
|
|
want: 1,
|
|
},
|
|
{
|
|
name: "caps at total episode count",
|
|
anime: domain.Anime{Anime: jikan.Anime{
|
|
Airing: true,
|
|
Episodes: 12,
|
|
Aired: jikan.Aired{From: "2026-04-04T15:00:00+00:00"},
|
|
}},
|
|
now: time.Date(2026, time.December, 1, 15, 0, 0, 0, time.UTC),
|
|
want: 12,
|
|
},
|
|
{
|
|
name: "unknown total still estimates current count",
|
|
anime: domain.Anime{Anime: jikan.Anime{
|
|
Airing: true,
|
|
Aired: jikan.Aired{From: "2026-04-04T15:00:00+00:00"},
|
|
}},
|
|
now: time.Date(2026, time.April, 18, 15, 0, 0, 0, time.UTC),
|
|
want: 3,
|
|
},
|
|
{
|
|
name: "non airing anime is not estimated",
|
|
anime: domain.Anime{Anime: jikan.Anime{
|
|
Airing: false,
|
|
Aired: jikan.Aired{From: "2026-04-04T15:00:00+00:00"},
|
|
}},
|
|
now: time.Date(2026, time.April, 18, 15, 0, 0, 0, time.UTC),
|
|
want: 0,
|
|
},
|
|
{
|
|
name: "invalid aired date is ignored",
|
|
anime: domain.Anime{Anime: jikan.Anime{
|
|
Airing: true,
|
|
Aired: jikan.Aired{From: "not-a-date"},
|
|
}},
|
|
now: time.Date(2026, time.April, 18, 15, 0, 0, 0, time.UTC),
|
|
want: 0,
|
|
},
|
|
}
|
|
|
|
func TestReleasedEpisodeCount(t *testing.T) {
|
|
for _, tt := range releasedCountTests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := releasedEpisodeCount(tt.anime, tt.now)
|
|
if got != tt.want {
|
|
t.Fatalf("releasedEpisodeCount() = %d, want %d", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestListedEpisodeCount(t *testing.T) {
|
|
episodes := []domain.EpisodeData{
|
|
{MalID: 1, Title: "Episode 1"},
|
|
{MalID: 2, Title: "Episode 2"},
|
|
{MalID: 3, Title: "Recap", IsRecap: true},
|
|
{Title: "missing id"},
|
|
}
|
|
|
|
got := listedEpisodeCount(episodes)
|
|
if got != 2 {
|
|
t.Fatalf("listedEpisodeCount() = %d, want 2", got)
|
|
}
|
|
}
|
|
|
|
func TestAnimeEpisodeCountUsesCanonicalEpisodes(t *testing.T) {
|
|
episodeSvc := &stubEpisodeService{
|
|
episodes: domain.CanonicalEpisodeList{
|
|
Source: "AllAnime",
|
|
Episodes: []domain.CanonicalEpisode{
|
|
{Number: 1},
|
|
{Number: 2},
|
|
{Number: 3},
|
|
},
|
|
},
|
|
}
|
|
handler := NewAnimeHandler(nil, nil, episodeSvc)
|
|
|
|
got := handler.animeEpisodeCount(context.Background(), domain.Anime{Anime: jikan.Anime{
|
|
MalID: 59970,
|
|
Airing: true,
|
|
Episodes: 12,
|
|
Aired: jikan.Aired{From: "2026-04-03T00:00:00+00:00"},
|
|
}}, time.Date(2026, time.June, 21, 0, 0, 0, 0, time.UTC))
|
|
|
|
if got.Count != 3 || got.Label != "Available episodes" {
|
|
t.Fatalf("animeEpisodeCount() = %+v, want count=3 label=%q", got, "Available episodes")
|
|
}
|
|
if episodeSvc.called != 1 {
|
|
t.Fatalf("GetCanonicalEpisodes() calls = %d, want 1", episodeSvc.called)
|
|
}
|
|
if episodeSvc.forceRefresh {
|
|
t.Fatal("animeEpisodeCount() should use fresh cache when available")
|
|
}
|
|
}
|
|
|
|
func TestAnimeEpisodeCountFallsBackToMetadata(t *testing.T) {
|
|
episodeSvc := &stubEpisodeService{err: errors.New("provider unavailable")}
|
|
handler := NewAnimeHandler(nil, nil, episodeSvc)
|
|
|
|
got := handler.animeEpisodeCount(context.Background(), domain.Anime{Anime: jikan.Anime{
|
|
MalID: 59970,
|
|
Airing: false,
|
|
Episodes: 12,
|
|
}}, time.Date(2026, time.June, 21, 0, 0, 0, 0, time.UTC))
|
|
|
|
if got.Count != 12 || got.Label != "Total episodes" {
|
|
t.Fatalf("animeEpisodeCount() = %+v, want count=12 label=%q", got, "Total episodes")
|
|
}
|
|
}
|
|
|
|
func TestAnimeAudioAvailabilityLabel(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
episodes []domain.CanonicalEpisode
|
|
want string
|
|
}{
|
|
{
|
|
name: "dub availability",
|
|
episodes: []domain.CanonicalEpisode{
|
|
{Number: 1, HasSub: true, HasDub: true},
|
|
},
|
|
want: "Dub available",
|
|
},
|
|
{
|
|
name: "subtitled availability",
|
|
episodes: []domain.CanonicalEpisode{
|
|
{Number: 1, HasSub: true, SubOnly: true},
|
|
},
|
|
want: "Subtitled only",
|
|
},
|
|
{
|
|
name: "unknown availability",
|
|
episodes: []domain.CanonicalEpisode{{Number: 1}},
|
|
want: "",
|
|
},
|
|
{
|
|
name: "no episodes",
|
|
episodes: []domain.CanonicalEpisode{},
|
|
want: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := animeAudioAvailabilityLabel(tt.episodes)
|
|
if got != tt.want {
|
|
t.Fatalf("animeAudioAvailabilityLabel() = %q, want %q", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAnimeAudioAvailabilityRequiresAllAnimeSource(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
source string
|
|
err error
|
|
want string
|
|
}{
|
|
{
|
|
name: "allanime source",
|
|
source: "AllAnime",
|
|
want: "Dub available",
|
|
},
|
|
{
|
|
name: "jikan fallback source",
|
|
source: "jikan_fallback",
|
|
want: "",
|
|
},
|
|
{
|
|
name: "legacy source",
|
|
source: "legacy_disabled",
|
|
want: "",
|
|
},
|
|
{
|
|
name: "provider error",
|
|
err: errors.New("provider unavailable"),
|
|
want: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
episodeSvc := &stubEpisodeService{
|
|
episodes: domain.CanonicalEpisodeList{
|
|
Source: tt.source,
|
|
Episodes: []domain.CanonicalEpisode{
|
|
{Number: 1, HasSub: true, HasDub: true},
|
|
},
|
|
},
|
|
err: tt.err,
|
|
}
|
|
handler := NewAnimeHandler(nil, nil, episodeSvc)
|
|
|
|
got := handler.animeAudioAvailability(context.Background(), domain.Anime{
|
|
Anime: jikan.Anime{MalID: 52991},
|
|
})
|
|
if got != tt.want {
|
|
t.Fatalf("animeAudioAvailability() = %q, want %q", got, tt.want)
|
|
}
|
|
if !episodeSvc.forceRefresh {
|
|
t.Fatal("animeAudioAvailability() did not force provider refresh")
|
|
}
|
|
})
|
|
}
|
|
}
|