package web import ( "bytes" "errors" "log" "net/http" "net/http/httptest" "net/url" "strings" "testing" "time" "gitea.kazu634.com/kazu634/tver/internal/store" "gitea.kazu634.com/kazu634/tver/internal/tver" ) func TestParseSeriesURL(t *testing.T) { tests := []struct { name, raw, want string valid bool }{ {"without www", "https://tver.jp/series/sr123", "sr123", true}, {"with www", "https://www.tver.jp/series/ABC123", "ABC123", true}, {"uppercase host", "https://TVER.JP/series/sr123", "sr123", true}, {"trailing slash", "https://tver.jp/series/sr123/", "sr123", true}, {"query and fragment", "https://tver.jp/series/sr123/?foo=bar#section", "sr123", true}, {"surrounding whitespace", " \thttps://tver.jp/series/sr123\n", "sr123", true}, {"http", "http://tver.jp/series/sr123", "", false}, {"subdomain suffix", "https://tver.jp.example.com/series/sr123", "", false}, {"evil prefix", "https://eviltver.jp/series/sr123", "", false}, {"userinfo on allowed host", "https://evil@tver.jp/series/sr123", "", false}, {"allowed host as userinfo", "https://tver.jp@evil.example/series/sr123", "", false}, {"episode", "https://tver.jp/episodes/ep123", "", false}, {"lp", "https://tver.jp/lp/example", "", false}, {"corner", "https://tver.jp/corner/example", "", false}, {"feature", "https://tver.jp/feature/example", "", false}, {"missing id", "https://tver.jp/series/", "", false}, {"invalid id", "https://tver.jp/series/sr_123", "", false}, {"extra path", "https://tver.jp/series/sr123/episodes", "", false}, {"encoded slash in id", "https://tver.jp/series/sr123%2Fevil", "", false}, {"encoded dot segments", "https://tver.jp/series/%2E%2E/episodes", "", false}, {"duplicate slash", "https://tver.jp//series/sr123", "", false}, {"port", "https://tver.jp:443/series/sr123", "", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := parseSeriesURL(tt.raw) if tt.valid { if err != nil || got != tt.want { t.Fatalf("parseSeriesURL(%q)=%q, %v; want %q", tt.raw, got, err, tt.want) } return } if err == nil || err.Error() != seriesURLInputError { t.Fatalf("parseSeriesURL(%q)=%q, %v; want input error", tt.raw, got, err) } }) } } func TestSubscriptionsPageShowsSeriesURLForm(t *testing.T) { page := getPage(newTestHandlerWithStore(&fakeFetcher{}, &fakeJobStore{}, nil), "/subscriptions") body := page.Body.String() if page.Code != http.StatusOK || !strings.Contains(body, `name="series_url"`) || !strings.Contains(body, `hx-post="/subscriptions/from-url/confirm"`) || !strings.Contains(body, `id="subscriptions-list"`) { t.Fatalf("URL入力フォーム: status=%d body=%s", page.Code, body) } } func TestConfirmSubscriptionURL(t *testing.T) { fetcher := &fakeFetcher{ seriesTitle: ``, seriesEpisodes: []tver.Episode{ {ID: "ep1", SeriesID: "sr1"}, {ID: "ep2", SeriesID: "sr1"}, }, } handler := newTestHandlerWithStore(fetcher, &fakeJobStore{}, nil) response := postForm(handler, "/subscriptions/from-url/confirm", url.Values{"series_url": {"https://tver.jp/series/sr1?x=1#fragment"}}) body := response.Body.String() for _, want := range []string{"<script>", "番組ID: sr1", "公開中の回数: 2件", `name="series_id" value="sr1"`, "公開中の回もダウンロードする", ">登録", ">キャンセル"} { if !strings.Contains(body, want) { t.Errorf("確認画面に %q がない: %s", want, body) } } if response.Code != http.StatusOK || strings.Contains(body, `