200 lines
11 KiB
Go
200 lines
11 KiB
Go
package web
|
||
|
||
import (
|
||
"errors"
|
||
"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 TestRegisterSubscriptionChoicesAndServerOwnedSeries(t *testing.T) {
|
||
for _, download := range []bool{false, true} {
|
||
t.Run(map[bool]string{false: "without existing", true: "with existing"}[download], func(t *testing.T) {
|
||
fetcher := &fakeFetcher{episodes: []tver.Episode{{ID: "ep1", SeriesID: "sr1", SeriesTitle: "本当の番組", Duration: 200}},
|
||
seriesEpisodes: []tver.Episode{{ID: "ep1", SeriesID: "sr1", SeriesTitle: "本当の番組", Title: "公開中", Duration: 200}}}
|
||
jobs := &fakeJobStore{}
|
||
handler := newTestHandlerWithStore(fetcher, jobs, nil)
|
||
page := getPage(handler, "/")
|
||
if page.Code != http.StatusOK || !strings.Contains(page.Body.String(), "番組を登録") || jobs.subscribedCalls != 1 || len(jobs.subscribedIDs) != 1 || jobs.subscribedIDs[0] != "sr1" {
|
||
t.Fatalf("新着の登録状態: %d ids=%v body=%s", page.Code, jobs.subscribedIDs, page.Body.String())
|
||
}
|
||
choice := getPage(handler, "/subscriptions/choice?genre=drama&episode_id=ep1")
|
||
if choice.Code != http.StatusOK || !strings.Contains(choice.Body.String(), "公開中の回もダウンロードする") || !strings.Contains(choice.Body.String(), ">登録</button>") {
|
||
t.Fatalf("登録時の選択: %d %s", choice.Code, choice.Body.String())
|
||
}
|
||
values := url.Values{"genre": {"drama"}, "episode_id": {"ep1"}, "series_id": {"forged"}, "series_title": {"偽番組"}}
|
||
if download {
|
||
values.Set("download_existing", "1")
|
||
}
|
||
registered := postForm(handler, "/subscriptions/register", values)
|
||
if registered.Code != http.StatusOK || !strings.Contains(registered.Body.String(), "登録済み") {
|
||
t.Fatalf("登録応答: %d %s", registered.Code, registered.Body.String())
|
||
}
|
||
if jobs.registerCalls != 1 || jobs.registerDownload != download || len(jobs.registerEpisodes) != 1 || jobs.registerEpisodes[0].ID != "ep1" || jobs.registerEpisodes[0].SeriesID != "sr1" || jobs.subscriptions["sr1"].SeriesTitle != "本当の番組" {
|
||
t.Fatalf("登録内容: calls=%d download=%t episodes=%+v subs=%+v", jobs.registerCalls, jobs.registerDownload, jobs.registerEpisodes, jobs.subscriptions)
|
||
}
|
||
if len(fetcher.seriesCalls) != 1 || fetcher.seriesCalls[0] != "sr1" {
|
||
t.Fatalf("番組一覧取得: %v", fetcher.seriesCalls)
|
||
}
|
||
if !strings.Contains(getPage(handler, "/subscriptions").Body.String(), "最終確認: 2026-10-03 09:00 JST") {
|
||
t.Fatal("登録直後の最終確認日時が表示されない")
|
||
}
|
||
fetcher.seriesErr = errors.New("API unavailable")
|
||
duplicate := postForm(handler, "/subscriptions/register", values)
|
||
if duplicate.Code != http.StatusOK || !strings.Contains(duplicate.Body.String(), "登録済み") || len(fetcher.seriesCalls) != 1 || jobs.registerCalls != 1 {
|
||
t.Fatalf("重複登録が API を再取得: %d %s calls=%v register=%d", duplicate.Code, duplicate.Body.String(), fetcher.seriesCalls, jobs.registerCalls)
|
||
}
|
||
reloaded := getPage(handler, "/")
|
||
if !strings.Contains(reloaded.Body.String(), "登録済み") || strings.Contains(reloaded.Body.String(), ">番組を登録</button>") {
|
||
t.Fatalf("登録済みの表示: %s", reloaded.Body.String())
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestRegisterSubscriptionRejectsMissingCacheAndFetchFailure(t *testing.T) {
|
||
fetcher := &fakeFetcher{episodes: []tver.Episode{{ID: "ep1", SeriesID: "sr1", SeriesTitle: "番組", Duration: 200}}}
|
||
jobs := &fakeJobStore{}
|
||
handler := newTestHandlerWithStore(fetcher, jobs, nil)
|
||
values := url.Values{"genre": {"drama"}, "episode_id": {"ep1"}}
|
||
if got := postForm(handler, "/subscriptions/register", values).Code; got != http.StatusNotFound {
|
||
t.Fatalf("キャッシュ無しの登録: %d", got)
|
||
}
|
||
getPage(handler, "/")
|
||
for _, path := range []string{"/subscriptions/register", "/subscriptions/sr1/pause", "/subscriptions/sr1/resume", "/subscriptions/sr1/delete"} {
|
||
response := getPage(handler, path)
|
||
if response.Code != http.StatusMethodNotAllowed || !strings.Contains(response.Header().Get("Allow"), "POST") {
|
||
t.Errorf("GET での変更 %s: status=%d Allow=%q", path, response.Code, response.Header().Get("Allow"))
|
||
}
|
||
}
|
||
for _, tt := range []struct {
|
||
path string
|
||
want int
|
||
}{
|
||
{"/subscriptions/choice?genre=news&episode_id=ep1", http.StatusBadRequest},
|
||
{"/subscriptions/choice?genre=drama", http.StatusBadRequest},
|
||
{"/subscriptions/choice?genre=drama&episode_id=missing", http.StatusNotFound},
|
||
} {
|
||
response := getPage(handler, tt.path)
|
||
if response.Code != tt.want {
|
||
t.Errorf("不正な選択 %s: got=%d want=%d", tt.path, response.Code, tt.want)
|
||
}
|
||
}
|
||
for _, tt := range []struct {
|
||
values url.Values
|
||
want int
|
||
}{
|
||
{url.Values{"genre": {"news"}, "episode_id": {"ep1"}}, http.StatusBadRequest},
|
||
{url.Values{"genre": {"drama"}}, http.StatusBadRequest},
|
||
{url.Values{"genre": {"drama"}, "episode_id": {"missing"}}, http.StatusNotFound},
|
||
} {
|
||
response := postForm(handler, "/subscriptions/register", tt.values)
|
||
if response.Code != tt.want {
|
||
t.Errorf("不正な登録 %v: got=%d want=%d", tt.values, response.Code, tt.want)
|
||
}
|
||
}
|
||
values.Set("download_existing", "bad")
|
||
if got := postForm(handler, "/subscriptions/register", values).Code; got != http.StatusBadRequest {
|
||
t.Fatalf("不正な選択: %d", got)
|
||
}
|
||
values.Del("download_existing")
|
||
fetcher.seriesErr = errors.New("platform_token=secret& upstream failure")
|
||
failed := postForm(handler, "/subscriptions/register", values)
|
||
if failed.Code != http.StatusOK || !strings.Contains(failed.Body.String(), "公開中の回を取得できませんでした") || strings.Contains(failed.Body.String(), "secret") || jobs.registerCalls != 0 {
|
||
t.Fatalf("取得失敗時に未登録: %d %s calls=%d", failed.Code, failed.Body.String(), jobs.registerCalls)
|
||
}
|
||
fetcher.seriesErr = nil
|
||
fetcher.seriesEpisodes = []tver.Episode{{ID: "ep1", SeriesID: "sr1", SeriesTitle: "番組", Duration: 200}}
|
||
jobs.registerErr = errors.New("secret database detail")
|
||
failed = postForm(handler, "/subscriptions/register", values)
|
||
if failed.Code != http.StatusOK || !strings.Contains(failed.Body.String(), "番組を登録できませんでした") || strings.Contains(failed.Body.String(), "secret") || len(jobs.subscriptions) != 0 {
|
||
t.Fatalf("DB 失敗時に未登録: %d %s subscriptions=%v", failed.Code, failed.Body.String(), jobs.subscriptions)
|
||
}
|
||
}
|
||
|
||
func TestNewerShowsInitiallySubscribedSeriesOncePerSeriesLookup(t *testing.T) {
|
||
fetcher := &fakeFetcher{episodes: []tver.Episode{
|
||
{ID: "ep1", SeriesID: "sr1", SeriesTitle: "番組", Duration: 200},
|
||
{ID: "ep2", SeriesID: "sr1", SeriesTitle: "番組", Duration: 200},
|
||
}}
|
||
jobs := &fakeJobStore{subscriptions: map[string]store.Subscription{
|
||
"sr1": {SeriesID: "sr1", SeriesTitle: "番組", Enabled: true},
|
||
}}
|
||
response := getPage(newTestHandlerWithStore(fetcher, jobs, nil), "/")
|
||
body := response.Body.String()
|
||
if response.Code != http.StatusOK || strings.Count(body, "登録済み") != 2 || strings.Contains(body, ">番組を登録</button>") || jobs.subscribedCalls != 1 || len(jobs.subscribedIDs) != 1 || jobs.subscribedIDs[0] != "sr1" {
|
||
t.Fatalf("登録済み番組の初期表示: status=%d calls=%d ids=%v body=%s", response.Code, jobs.subscribedCalls, jobs.subscribedIDs, body)
|
||
}
|
||
}
|
||
|
||
func TestSubscriptionMutationsWithoutHtmxRedirect(t *testing.T) {
|
||
fetcher := &fakeFetcher{
|
||
episodes: []tver.Episode{{ID: "ep1", SeriesID: "sr1", SeriesTitle: "番組", Duration: 200}},
|
||
seriesEpisodes: []tver.Episode{{ID: "ep1", SeriesID: "sr1", SeriesTitle: "番組", Duration: 200}},
|
||
}
|
||
jobs := &fakeJobStore{}
|
||
handler := newTestHandlerWithStore(fetcher, jobs, nil)
|
||
getPage(handler, "/")
|
||
form := url.Values{"genre": {"drama"}, "episode_id": {"ep1"}}
|
||
request := httptest.NewRequest(http.MethodPost, "/subscriptions/register", strings.NewReader(form.Encode()))
|
||
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||
response := httptest.NewRecorder()
|
||
handler.ServeHTTP(response, request)
|
||
if response.Code != http.StatusSeeOther || response.Header().Get("Location") != "/?genre=drama" {
|
||
t.Fatalf("通常登録 POST: status=%d Location=%q", response.Code, response.Header().Get("Location"))
|
||
}
|
||
request = httptest.NewRequest(http.MethodPost, "/subscriptions/sr1/pause", nil)
|
||
response = httptest.NewRecorder()
|
||
handler.ServeHTTP(response, request)
|
||
if response.Code != http.StatusSeeOther || response.Header().Get("Location") != "/subscriptions" || jobs.subscriptions["sr1"].Enabled {
|
||
t.Fatalf("通常操作 POST: status=%d Location=%q subscription=%+v", response.Code, response.Header().Get("Location"), jobs.subscriptions["sr1"])
|
||
}
|
||
}
|
||
|
||
func TestSubscriptionsPagePauseResumeDeleteAndEscaping(t *testing.T) {
|
||
checked := time.Date(2026, 10, 4, 2, 30, 0, 0, time.UTC)
|
||
failedAt := time.Date(2026, 10, 4, 3, 45, 0, 0, time.UTC)
|
||
checkError := `<script>alert("error")</script> token=[REDACTED]`
|
||
jobs := &fakeJobStore{subscriptions: map[string]store.Subscription{
|
||
"sr1": {SeriesID: "sr1", SeriesTitle: `<script>alert(1)</script>`, Enabled: true,
|
||
CreatedAt: time.Date(2026, 10, 3, 0, 0, 0, 0, time.UTC), LastCheckedAt: &checked,
|
||
LastCheckErrorAt: &failedAt, LastCheckError: &checkError, JobCount: 3},
|
||
"sr2": {SeriesID: "sr2", SeriesTitle: "別番組", Enabled: false, JobCount: 0},
|
||
}}
|
||
handler := newTestHandlerWithStore(&fakeFetcher{}, jobs, nil)
|
||
page := getPage(handler, "/subscriptions")
|
||
if page.Code != http.StatusOK {
|
||
t.Fatalf("登録番組ページ: %d", page.Code)
|
||
}
|
||
body := page.Body.String()
|
||
for _, want := range []string{"<script>", "登録日時: 2026-10-03 09:00 JST", "最終確認: 2026-10-04 11:30 JST", "最終確認: 未確認", "最後の確認エラー(2026-10-04 12:45 JST)", `<script>alert("error")</script> token=[REDACTED]`, "関連ジョブ: 3件", "有効", "一時停止", `hx-confirm="番組の登録を解除しますか?"`} {
|
||
if !strings.Contains(body, want) {
|
||
t.Errorf("登録番組の情報 %q がない", want)
|
||
}
|
||
}
|
||
if strings.Contains(body, `<script>`) {
|
||
t.Fatal("番組名または確認エラーがエスケープされていない")
|
||
}
|
||
paused := postForm(handler, "/subscriptions/sr1/pause", nil)
|
||
if paused.Code != http.StatusOK || jobs.subscriptions["sr1"].Enabled || !strings.Contains(paused.Body.String(), `action="/subscriptions/sr1/resume"`) || strings.Contains(paused.Body.String(), "<html") {
|
||
t.Fatalf("一時停止: %d %s", paused.Code, paused.Body.String())
|
||
}
|
||
resumed := postForm(handler, "/subscriptions/sr1/resume", nil)
|
||
if resumed.Code != http.StatusOK || !jobs.subscriptions["sr1"].Enabled || !strings.Contains(resumed.Body.String(), `action="/subscriptions/sr1/pause"`) {
|
||
t.Fatalf("再開: %d %s", resumed.Code, resumed.Body.String())
|
||
}
|
||
deleted := postForm(handler, "/subscriptions/sr1/delete", nil)
|
||
if deleted.Code != http.StatusOK || len(jobs.subscriptions) != 1 || strings.Contains(deleted.Body.String(), "<script>") {
|
||
t.Fatalf("解除: %d %s", deleted.Code, deleted.Body.String())
|
||
}
|
||
if got := postForm(handler, "/subscriptions/sr1/delete", nil).Code; got != http.StatusNotFound {
|
||
t.Fatalf("存在しない番組の解除: %d", got)
|
||
}
|
||
}
|