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, `