Идентичность на ULID: download_infohash, guarded-дедуп, миграция (ulid-identity)
Все сущности переехали с INTEGER AUTOINCREMENT на TEXT ULID (lowercase, internal/ident — единая точка генерации и разбора; oklog/ulid). Инфохэши загрузки — множество (download_infohash, v1/v2 гибридных торрентов): дедуп и сопоставление в поллинге по любому из хешей, magnet-парсер отдаёт оба хеша гибридной ссылки, усечённый v2-хеш v2-only раздач не хранится. Инвариант «не более одной активной загрузки на infohash» вместо снятого unique-индекса держат guarded-методы store в одной write-транзакции (_txlock=immediate): CreateDownloadIfNoActive (приём/adopt, с доносом недостающих хешей), ActivateIfNoOtherActive (retry/recovery/relink, отказ до побочных эффектов), guarded AddInfohashes; SetDownloadState отклоняет терминал→активное как механический бэкстоп. Миграция 0006 — первая Go-миграция goose: пересоздание таблиц при включённых FK, backfill ULID с timestamp из created_at (хронология id сохранена), разнос infohash, удаление idempotency_key. BREAKING: формат id в URL/логах/Telegram, REST-поля id (string) и infohashes (список). Новая конвенция docs/conventions/database.md (без числовых PK), корреляция в логах grep'ом по голому ULID, ER-схема обновлена. Спеки: новая capability identity, MODIFIED в state-reconciliation; change заархивирован. Пройдены ревью дизайна и кода (по 8 углов), все находки исправлены с регрессионными тестами. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,14 @@ import (
|
||||
"git.vakhrushev.me/av/jellybit/internal/worker"
|
||||
)
|
||||
|
||||
// Валидные lowercase-ULID для маршрутов (pathID валидирует формат).
|
||||
const (
|
||||
tid = "01arz3ndektsv4rrffq69g5fav"
|
||||
tid2 = "01arz3ndektsv4rrffq69g5fb0"
|
||||
cid = "01arz3ndektsv4rrffq69g5fc0"
|
||||
cid2 = "01arz3ndektsv4rrffq69g5fd0"
|
||||
)
|
||||
|
||||
type fakeIngestor struct {
|
||||
res ingest.Result
|
||||
err error
|
||||
@@ -33,12 +41,12 @@ func (f *fakeIngestor) Ingest(_ context.Context, req ingest.Request) (ingest.Res
|
||||
}
|
||||
|
||||
type fakeCommander struct {
|
||||
cancelled []int64
|
||||
retried []int64
|
||||
cancelled []string
|
||||
retried []string
|
||||
err error
|
||||
}
|
||||
|
||||
func (f *fakeCommander) Cancel(_ context.Context, id int64) error {
|
||||
func (f *fakeCommander) Cancel(_ context.Context, id string) error {
|
||||
if f.err != nil {
|
||||
return f.err
|
||||
}
|
||||
@@ -46,7 +54,7 @@ func (f *fakeCommander) Cancel(_ context.Context, id int64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeCommander) Retry(_ context.Context, id int64) error {
|
||||
func (f *fakeCommander) Retry(_ context.Context, id string) error {
|
||||
if f.err != nil {
|
||||
return f.err
|
||||
}
|
||||
@@ -76,7 +84,7 @@ func (f *fakeReader) ListDownloadsPage(_ context.Context, flt store.ListFilter)
|
||||
return f.list, total, nil
|
||||
}
|
||||
|
||||
func (f *fakeReader) GetDownload(_ context.Context, id int64) (*store.Download, error) {
|
||||
func (f *fakeReader) GetDownload(_ context.Context, id string) (*store.Download, error) {
|
||||
if f.get != nil {
|
||||
return f.get, nil
|
||||
}
|
||||
@@ -98,7 +106,7 @@ func newServer(t *testing.T, d httpapi.Deps) *httptest.Server {
|
||||
}
|
||||
|
||||
func TestAPIAdd(t *testing.T) {
|
||||
ing := &fakeIngestor{res: ingest.Result{DownloadID: 1, Infohash: "abc", State: store.StateDownloading}}
|
||||
ing := &fakeIngestor{res: ingest.Result{DownloadID: tid, Infohashes: []string{"abc"}, State: store.StateDownloading}}
|
||||
srv := newServer(t, httpapi.Deps{Ingestor: ing, Commander: &fakeCommander{}, Reader: &fakeReader{}})
|
||||
|
||||
resp, err := http.Post(srv.URL+"/api/downloads", "application/json",
|
||||
@@ -112,7 +120,7 @@ func TestAPIAdd(t *testing.T) {
|
||||
}
|
||||
var got map[string]any
|
||||
_ = json.NewDecoder(resp.Body).Decode(&got)
|
||||
if got["id"].(float64) != 1 || got["state"] != "downloading" {
|
||||
if got["id"] != tid || got["state"] != "downloading" {
|
||||
t.Errorf("body = %v", got)
|
||||
}
|
||||
if ing.lastReq.Context != "Дюна" {
|
||||
@@ -138,8 +146,9 @@ func TestAPIAddBadInput(t *testing.T) {
|
||||
|
||||
func TestAPIList(t *testing.T) {
|
||||
reader := &fakeReader{list: []store.Download{
|
||||
{ID: 2, SourceType: store.SourceMagnet, State: store.StateCompleted, Infohash: store.NullString("abc")},
|
||||
{ID: 1, SourceType: store.SourceMagnet, State: store.StateDownloading},
|
||||
{ID: tid2, SourceType: store.SourceMagnet, State: store.StateCompleted,
|
||||
Infohashes: []store.Infohash{{DownloadID: tid2, Infohash: "abc", Kind: store.HashV1}}},
|
||||
{ID: tid, SourceType: store.SourceMagnet, State: store.StateDownloading},
|
||||
}}
|
||||
srv := newServer(t, httpapi.Deps{Ingestor: &fakeIngestor{}, Commander: &fakeCommander{}, Reader: reader})
|
||||
|
||||
@@ -153,7 +162,8 @@ func TestAPIList(t *testing.T) {
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("len = %d, want 2", len(got))
|
||||
}
|
||||
if got[0]["state"] != "completed" || got[0]["infohash"] != "abc" {
|
||||
hashes, _ := got[0]["infohashes"].([]any)
|
||||
if got[0]["state"] != "completed" || len(hashes) != 1 || hashes[0] != "abc" {
|
||||
t.Errorf("first = %v", got[0])
|
||||
}
|
||||
}
|
||||
@@ -162,7 +172,7 @@ func TestAPICancel(t *testing.T) {
|
||||
cmd := &fakeCommander{}
|
||||
srv := newServer(t, httpapi.Deps{Ingestor: &fakeIngestor{}, Commander: cmd, Reader: &fakeReader{}})
|
||||
|
||||
resp, err := http.Post(srv.URL+"/api/downloads/5/cancel", "", nil)
|
||||
resp, err := http.Post(srv.URL+"/api/downloads/"+tid+"/cancel", "", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -170,7 +180,7 @@ func TestAPICancel(t *testing.T) {
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200", resp.StatusCode)
|
||||
}
|
||||
if len(cmd.cancelled) != 1 || cmd.cancelled[0] != 5 {
|
||||
if len(cmd.cancelled) != 1 || cmd.cancelled[0] != tid {
|
||||
t.Errorf("cancel вызван неверно: %v", cmd.cancelled)
|
||||
}
|
||||
}
|
||||
@@ -179,7 +189,7 @@ func TestUIRetry(t *testing.T) {
|
||||
cmd := &fakeCommander{}
|
||||
srv := newServer(t, httpapi.Deps{Ingestor: &fakeIngestor{}, Commander: cmd, Reader: &fakeReader{}})
|
||||
|
||||
resp, err := http.Post(srv.URL+"/ui/downloads/5/retry", "application/x-www-form-urlencoded", nil)
|
||||
resp, err := http.Post(srv.URL+"/ui/downloads/"+tid+"/retry", "application/x-www-form-urlencoded", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -187,7 +197,7 @@ func TestUIRetry(t *testing.T) {
|
||||
if resp.StatusCode != http.StatusOK { // 303 → редирект на / → 200
|
||||
t.Fatalf("status = %d, want 200", resp.StatusCode)
|
||||
}
|
||||
if len(cmd.retried) != 1 || cmd.retried[0] != 5 {
|
||||
if len(cmd.retried) != 1 || cmd.retried[0] != tid {
|
||||
t.Errorf("retry вызван неверно: %v", cmd.retried)
|
||||
}
|
||||
}
|
||||
@@ -197,7 +207,7 @@ func TestAPICommandConflict(t *testing.T) {
|
||||
cmd := &fakeCommander{err: fmt.Errorf("cancel: download 5 in wrong state: %w", worker.ErrConflict)}
|
||||
srv := newServer(t, httpapi.Deps{Ingestor: &fakeIngestor{}, Commander: cmd, Reader: &fakeReader{}})
|
||||
|
||||
resp, err := http.Post(srv.URL+"/api/downloads/5/cancel", "", nil)
|
||||
resp, err := http.Post(srv.URL+"/api/downloads/"+tid+"/cancel", "", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -207,14 +217,14 @@ func TestAPICommandConflict(t *testing.T) {
|
||||
}
|
||||
var got map[string]any
|
||||
_ = json.NewDecoder(resp.Body).Decode(&got)
|
||||
if got["download_id"].(float64) != 5 {
|
||||
if got["download_id"] != tid {
|
||||
t.Errorf("download_id корреляции нет: %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexRenders(t *testing.T) {
|
||||
reader := &fakeReader{list: []store.Download{
|
||||
{ID: 1, SourceType: store.SourceMagnet, SourceRef: "magnet:?xt=urn:btih:abc", State: store.StateDownloading},
|
||||
{ID: tid, SourceType: store.SourceMagnet, SourceRef: "magnet:?xt=urn:btih:abc", State: store.StateDownloading},
|
||||
}}
|
||||
srv := newServer(t, httpapi.Deps{Ingestor: &fakeIngestor{}, Commander: &fakeCommander{}, Reader: reader})
|
||||
|
||||
@@ -239,7 +249,7 @@ func (e ingestErr) Error() string { return string(e) }
|
||||
func TestIndexTitleFromDisplayName(t *testing.T) {
|
||||
// Заголовок карточки — имя раздачи (display_name), а не сырой magnet.
|
||||
reader := &fakeReader{list: []store.Download{{
|
||||
ID: 1, SourceType: store.SourceMagnet, SourceRef: "magnet:?xt=urn:btih:abc",
|
||||
ID: tid, SourceType: store.SourceMagnet, SourceRef: "magnet:?xt=urn:btih:abc",
|
||||
DisplayName: "Дюна: Часть вторая (2024)", State: store.StateDownloading,
|
||||
}}}
|
||||
srv := newServer(t, httpapi.Deps{Ingestor: &fakeIngestor{}, Commander: &fakeCommander{}, Reader: reader})
|
||||
@@ -301,11 +311,11 @@ func TestIndexPageClamp(t *testing.T) {
|
||||
func detailReviewData(provider, providerID, chosenID, chosenURL string) *worker.ReviewData {
|
||||
rd := &worker.ReviewData{
|
||||
Download: store.Download{
|
||||
ID: 1, State: store.StateReview, SourceType: store.SourceMagnet,
|
||||
ID: tid, State: store.StateReview, SourceType: store.SourceMagnet,
|
||||
SourceRef: "magnet:?xt=urn:btih:deadbeef", DisplayName: "Дюна",
|
||||
Infohash: store.NullString("deadbeef"),
|
||||
Infohashes: []store.Infohash{{DownloadID: tid, Infohash: "deadbeef", Kind: store.HashV1}},
|
||||
},
|
||||
Recognition: &store.Recognition{ID: 1, DownloadID: 1, IsCurrent: true},
|
||||
Recognition: &store.Recognition{ID: "1", DownloadID: tid, IsCurrent: true},
|
||||
Plan: recognize.Plan{
|
||||
Type: recognize.MediaMovie, Title: "Дюна", Year: 2024,
|
||||
Files: []recognize.PlanFile{{Src: "dune.mkv", Role: recognize.RoleMain}},
|
||||
@@ -316,7 +326,7 @@ func detailReviewData(provider, providerID, chosenID, chosenURL string) *worker.
|
||||
}
|
||||
if chosenID != "" {
|
||||
rd.Candidates = []store.MetadataCandidate{{
|
||||
ID: 10, Provider: "tmdb", ProviderID: chosenID, Chosen: true,
|
||||
ID: cid, Provider: "tmdb", ProviderID: chosenID, Chosen: true,
|
||||
URL: store.NullString(chosenURL),
|
||||
}}
|
||||
}
|
||||
@@ -329,7 +339,7 @@ func TestDownloadMatchLinkFromCandidate(t *testing.T) {
|
||||
srv := newServer(t, httpapi.Deps{Ingestor: &fakeIngestor{}, Commander: &fakeCommander{},
|
||||
Reader: &fakeReader{}, Reviewer: rv})
|
||||
|
||||
resp, err := http.Get(srv.URL + "/download/1")
|
||||
resp, err := http.Get(srv.URL + "/download/" + tid)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -347,7 +357,7 @@ func TestDownloadMatchLinkMismatchUsesBuilt(t *testing.T) {
|
||||
srv := newServer(t, httpapi.Deps{Ingestor: &fakeIngestor{}, Commander: &fakeCommander{},
|
||||
Reader: &fakeReader{}, Reviewer: rv})
|
||||
|
||||
resp, err := http.Get(srv.URL + "/download/1")
|
||||
resp, err := http.Get(srv.URL + "/download/" + tid)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -366,7 +376,7 @@ func TestDownloadTorrentInfoBlock(t *testing.T) {
|
||||
srv := newServer(t, httpapi.Deps{Ingestor: &fakeIngestor{}, Commander: &fakeCommander{},
|
||||
Reader: &fakeReader{}, Reviewer: rv})
|
||||
|
||||
resp, err := http.Get(srv.URL + "/download/1")
|
||||
resp, err := http.Get(srv.URL + "/download/" + tid)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -384,81 +394,81 @@ func TestDownloadTorrentInfoBlock(t *testing.T) {
|
||||
type fakeReviewer struct {
|
||||
data *worker.ReviewData
|
||||
applyErr error
|
||||
refined map[int64]string
|
||||
typed map[int64]string
|
||||
ignored map[int64]string
|
||||
chosen map[int64]int64
|
||||
providerSet map[int64]string
|
||||
applied []int64
|
||||
deferred []int64
|
||||
undone []int64
|
||||
relinked []int64
|
||||
rerecognized []int64
|
||||
cleared []int64
|
||||
refined map[string]string
|
||||
typed map[string]string
|
||||
ignored map[string]string
|
||||
chosen map[string]string
|
||||
providerSet map[string]string
|
||||
applied []string
|
||||
deferred []string
|
||||
undone []string
|
||||
relinked []string
|
||||
rerecognized []string
|
||||
cleared []string
|
||||
}
|
||||
|
||||
func (f *fakeReviewer) ReviewData(_ context.Context, _ int64) (*worker.ReviewData, error) {
|
||||
func (f *fakeReviewer) ReviewData(_ context.Context, _ string) (*worker.ReviewData, error) {
|
||||
return f.data, nil
|
||||
}
|
||||
func (f *fakeReviewer) Apply(_ context.Context, id int64) error {
|
||||
func (f *fakeReviewer) Apply(_ context.Context, id string) error {
|
||||
if f.applyErr != nil {
|
||||
return f.applyErr
|
||||
}
|
||||
f.applied = append(f.applied, id)
|
||||
return nil
|
||||
}
|
||||
func (f *fakeReviewer) Refine(_ context.Context, id int64, hint string) error {
|
||||
func (f *fakeReviewer) Refine(_ context.Context, id string, hint string) error {
|
||||
if f.refined == nil {
|
||||
f.refined = map[int64]string{}
|
||||
f.refined = map[string]string{}
|
||||
}
|
||||
f.refined[id] = hint
|
||||
return nil
|
||||
}
|
||||
func (f *fakeReviewer) SetType(_ context.Context, id int64, t string) error {
|
||||
func (f *fakeReviewer) SetType(_ context.Context, id string, t string) error {
|
||||
if f.typed == nil {
|
||||
f.typed = map[int64]string{}
|
||||
f.typed = map[string]string{}
|
||||
}
|
||||
f.typed[id] = t
|
||||
return nil
|
||||
}
|
||||
func (f *fakeReviewer) IgnoreFile(_ context.Context, id int64, src string) error {
|
||||
func (f *fakeReviewer) IgnoreFile(_ context.Context, id string, src string) error {
|
||||
if f.ignored == nil {
|
||||
f.ignored = map[int64]string{}
|
||||
f.ignored = map[string]string{}
|
||||
}
|
||||
f.ignored[id] = src
|
||||
return nil
|
||||
}
|
||||
func (f *fakeReviewer) Defer(_ context.Context, id int64) error {
|
||||
func (f *fakeReviewer) Defer(_ context.Context, id string) error {
|
||||
f.deferred = append(f.deferred, id)
|
||||
return nil
|
||||
}
|
||||
func (f *fakeReviewer) Undo(_ context.Context, id int64) error {
|
||||
func (f *fakeReviewer) Undo(_ context.Context, id string) error {
|
||||
f.undone = append(f.undone, id)
|
||||
return nil
|
||||
}
|
||||
func (f *fakeReviewer) Relink(_ context.Context, id int64) error {
|
||||
func (f *fakeReviewer) Relink(_ context.Context, id string) error {
|
||||
f.relinked = append(f.relinked, id)
|
||||
return nil
|
||||
}
|
||||
func (f *fakeReviewer) Rerecognize(_ context.Context, id int64) error {
|
||||
func (f *fakeReviewer) Rerecognize(_ context.Context, id string) error {
|
||||
f.rerecognized = append(f.rerecognized, id)
|
||||
return nil
|
||||
}
|
||||
func (f *fakeReviewer) ChooseCandidate(_ context.Context, id, candidateID int64) error {
|
||||
func (f *fakeReviewer) ChooseCandidate(_ context.Context, id, candidateID string) error {
|
||||
if f.chosen == nil {
|
||||
f.chosen = map[int64]int64{}
|
||||
f.chosen = map[string]string{}
|
||||
}
|
||||
f.chosen[id] = candidateID
|
||||
return nil
|
||||
}
|
||||
func (f *fakeReviewer) SetProviderID(_ context.Context, id int64, provider, providerID string) error {
|
||||
func (f *fakeReviewer) SetProviderID(_ context.Context, id string, provider, providerID string) error {
|
||||
if f.providerSet == nil {
|
||||
f.providerSet = map[int64]string{}
|
||||
f.providerSet = map[string]string{}
|
||||
}
|
||||
f.providerSet[id] = provider + ":" + providerID
|
||||
return nil
|
||||
}
|
||||
func (f *fakeReviewer) ClearProvider(_ context.Context, id int64) error {
|
||||
func (f *fakeReviewer) ClearProvider(_ context.Context, id string) error {
|
||||
f.cleared = append(f.cleared, id)
|
||||
return nil
|
||||
}
|
||||
@@ -466,9 +476,9 @@ func (f *fakeReviewer) ClearProvider(_ context.Context, id int64) error {
|
||||
func seriesReviewData() *worker.ReviewData {
|
||||
s, e := 2, 1
|
||||
return &worker.ReviewData{
|
||||
Download: store.Download{ID: 1, State: store.StateReview, SourceRef: "magnet:?xt=urn:btih:abc"},
|
||||
Download: store.Download{ID: tid, State: store.StateReview, SourceRef: "magnet:?xt=urn:btih:abc"},
|
||||
Recognition: &store.Recognition{
|
||||
ID: 1, DownloadID: 1, IsCurrent: true, Reasons: `["нет матча в базе"]`,
|
||||
ID: "1", DownloadID: tid, IsCurrent: true, Reasons: `["нет матча в базе"]`,
|
||||
},
|
||||
Plan: recognize.Plan{
|
||||
Type: recognize.MediaSeries, Title: "Фарго", Year: 2015,
|
||||
@@ -480,9 +490,9 @@ func seriesReviewData() *worker.ReviewData {
|
||||
{Src: "Fargo/e1.mkv", Dst: "/srv/media/series/Фарго (2015)/Season 02/Фарго (2015) S02E01.mkv"},
|
||||
},
|
||||
Candidates: []store.MetadataCandidate{
|
||||
{ID: 10, Provider: "tvdb", ProviderID: "269613", Title: store.NullString("Fargo"),
|
||||
{ID: cid, Provider: "tvdb", ProviderID: "269613", Title: store.NullString("Fargo"),
|
||||
Year: sql.NullInt64{Int64: 2014, Valid: true}},
|
||||
{ID: 11, Provider: "tmdb", ProviderID: "60622", Title: store.NullString("Fargo")},
|
||||
{ID: cid2, Provider: "tmdb", ProviderID: "60622", Title: store.NullString("Fargo")},
|
||||
},
|
||||
Hints: []string{"второй сезон"},
|
||||
}
|
||||
@@ -500,7 +510,7 @@ func TestReviewRenders(t *testing.T) {
|
||||
srv := newServer(t, httpapi.Deps{Ingestor: &fakeIngestor{}, Commander: &fakeCommander{},
|
||||
Reader: &fakeReader{}, Reviewer: rv})
|
||||
|
||||
resp, err := http.Get(srv.URL + "/review/1")
|
||||
resp, err := http.Get(srv.URL + "/review/" + tid)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -529,7 +539,7 @@ func TestReviewShowsMatchLink(t *testing.T) {
|
||||
srv := newServer(t, httpapi.Deps{Ingestor: &fakeIngestor{}, Commander: &fakeCommander{},
|
||||
Reader: &fakeReader{}, Reviewer: rv})
|
||||
|
||||
resp, err := http.Get(srv.URL + "/review/1")
|
||||
resp, err := http.Get(srv.URL + "/review/" + tid)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -545,16 +555,16 @@ func TestChooseCandidate(t *testing.T) {
|
||||
srv := newServer(t, httpapi.Deps{Ingestor: &fakeIngestor{}, Commander: &fakeCommander{},
|
||||
Reader: &fakeReader{}, Reviewer: rv})
|
||||
|
||||
resp, err := noRedirectClient().PostForm(srv.URL+"/ui/downloads/1/candidate",
|
||||
map[string][]string{"candidate_id": {"10"}})
|
||||
resp, err := noRedirectClient().PostForm(srv.URL+"/ui/downloads/"+tid+"/candidate",
|
||||
map[string][]string{"candidate_id": {cid}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if rv.chosen[1] != 10 {
|
||||
t.Errorf("ChooseCandidate получил %d", rv.chosen[1])
|
||||
if rv.chosen[tid] != cid {
|
||||
t.Errorf("ChooseCandidate получил %q", rv.chosen[tid])
|
||||
}
|
||||
if loc := resp.Header.Get("Location"); !strings.HasPrefix(loc, "/review/1") {
|
||||
if loc := resp.Header.Get("Location"); !strings.HasPrefix(loc, "/review/"+tid) {
|
||||
t.Errorf("Location = %q", loc)
|
||||
}
|
||||
}
|
||||
@@ -565,18 +575,18 @@ func TestSetProviderAndNoBase(t *testing.T) {
|
||||
Reader: &fakeReader{}, Reviewer: rv})
|
||||
cl := noRedirectClient()
|
||||
|
||||
if _, err := cl.PostForm(srv.URL+"/ui/downloads/1/provider",
|
||||
if _, err := cl.PostForm(srv.URL+"/ui/downloads/"+tid+"/provider",
|
||||
map[string][]string{"provider": {"tvdb"}, "provider_id": {"269613"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rv.providerSet[1] != "tvdb:269613" {
|
||||
t.Errorf("SetProviderID получил %q", rv.providerSet[1])
|
||||
if rv.providerSet[tid] != "tvdb:269613" {
|
||||
t.Errorf("SetProviderID получил %q", rv.providerSet[tid])
|
||||
}
|
||||
|
||||
if _, err := cl.Post(srv.URL+"/ui/downloads/1/nobase", "", nil); err != nil {
|
||||
if _, err := cl.Post(srv.URL+"/ui/downloads/"+tid+"/nobase", "", nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(rv.cleared) != 1 || rv.cleared[0] != 1 {
|
||||
if len(rv.cleared) != 1 || rv.cleared[0] != tid {
|
||||
t.Errorf("ClearProvider = %v", rv.cleared)
|
||||
}
|
||||
}
|
||||
@@ -586,7 +596,7 @@ func TestApplyRedirectsToIndex(t *testing.T) {
|
||||
srv := newServer(t, httpapi.Deps{Ingestor: &fakeIngestor{}, Commander: &fakeCommander{},
|
||||
Reader: &fakeReader{}, Reviewer: rv})
|
||||
|
||||
resp, err := noRedirectClient().Post(srv.URL+"/ui/downloads/1/apply", "", nil)
|
||||
resp, err := noRedirectClient().Post(srv.URL+"/ui/downloads/"+tid+"/apply", "", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -607,13 +617,13 @@ func TestApplyCollisionRedirectsToReview(t *testing.T) {
|
||||
srv := newServer(t, httpapi.Deps{Ingestor: &fakeIngestor{}, Commander: &fakeCommander{},
|
||||
Reader: &fakeReader{}, Reviewer: rv})
|
||||
|
||||
resp, err := noRedirectClient().Post(srv.URL+"/ui/downloads/1/apply", "", nil)
|
||||
resp, err := noRedirectClient().Post(srv.URL+"/ui/downloads/"+tid+"/apply", "", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if loc := resp.Header.Get("Location"); !strings.HasPrefix(loc, "/review/1") {
|
||||
t.Errorf("Location = %q, want /review/1?err=...", loc)
|
||||
if loc := resp.Header.Get("Location"); !strings.HasPrefix(loc, "/review/"+tid) {
|
||||
t.Errorf("Location = %q, want /review/{id}?err=...", loc)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -622,16 +632,16 @@ func TestRefinePostsHint(t *testing.T) {
|
||||
srv := newServer(t, httpapi.Deps{Ingestor: &fakeIngestor{}, Commander: &fakeCommander{},
|
||||
Reader: &fakeReader{}, Reviewer: rv})
|
||||
|
||||
resp, err := noRedirectClient().PostForm(srv.URL+"/ui/downloads/1/refine",
|
||||
resp, err := noRedirectClient().PostForm(srv.URL+"/ui/downloads/"+tid+"/refine",
|
||||
map[string][]string{"hint": {"это второй сезон"}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if rv.refined[1] != "это второй сезон" {
|
||||
t.Errorf("Refine получил %q", rv.refined[1])
|
||||
if rv.refined[tid] != "это второй сезон" {
|
||||
t.Errorf("Refine получил %q", rv.refined[tid])
|
||||
}
|
||||
if loc := resp.Header.Get("Location"); !strings.HasPrefix(loc, "/review/1") {
|
||||
if loc := resp.Header.Get("Location"); !strings.HasPrefix(loc, "/review/"+tid) {
|
||||
t.Errorf("Location = %q", loc)
|
||||
}
|
||||
}
|
||||
@@ -642,20 +652,20 @@ func TestIgnoreAndType(t *testing.T) {
|
||||
Reader: &fakeReader{}, Reviewer: rv})
|
||||
cl := noRedirectClient()
|
||||
|
||||
if _, err := cl.PostForm(srv.URL+"/ui/downloads/1/ignore",
|
||||
if _, err := cl.PostForm(srv.URL+"/ui/downloads/"+tid+"/ignore",
|
||||
map[string][]string{"src": {"Fargo/sample.mkv"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rv.ignored[1] != "Fargo/sample.mkv" {
|
||||
t.Errorf("IgnoreFile получил %q", rv.ignored[1])
|
||||
if rv.ignored[tid] != "Fargo/sample.mkv" {
|
||||
t.Errorf("IgnoreFile получил %q", rv.ignored[tid])
|
||||
}
|
||||
|
||||
if _, err := cl.PostForm(srv.URL+"/ui/downloads/1/type",
|
||||
if _, err := cl.PostForm(srv.URL+"/ui/downloads/"+tid+"/type",
|
||||
map[string][]string{"type": {"movie"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rv.typed[1] != "movie" {
|
||||
t.Errorf("SetType получил %q", rv.typed[1])
|
||||
if rv.typed[tid] != "movie" {
|
||||
t.Errorf("SetType получил %q", rv.typed[tid])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -665,10 +675,10 @@ func TestUndoAndDefer(t *testing.T) {
|
||||
Reader: &fakeReader{}, Reviewer: rv})
|
||||
cl := noRedirectClient()
|
||||
|
||||
if _, err := cl.Post(srv.URL+"/ui/downloads/1/undo", "", nil); err != nil {
|
||||
if _, err := cl.Post(srv.URL+"/ui/downloads/"+tid+"/undo", "", nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := cl.Post(srv.URL+"/ui/downloads/1/defer", "", nil); err != nil {
|
||||
if _, err := cl.Post(srv.URL+"/ui/downloads/"+tid+"/defer", "", nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(rv.undone) != 1 || len(rv.deferred) != 1 {
|
||||
@@ -682,11 +692,11 @@ func TestRelink(t *testing.T) {
|
||||
Reader: &fakeReader{}, Reviewer: rv})
|
||||
cl := noRedirectClient()
|
||||
|
||||
if _, err := cl.Post(srv.URL+"/ui/downloads/1/relink", "", nil); err != nil {
|
||||
if _, err := cl.Post(srv.URL+"/ui/downloads/"+tid+"/relink", "", nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(rv.relinked) != 1 || rv.relinked[0] != 1 {
|
||||
t.Errorf("relinked = %v, want [1]", rv.relinked)
|
||||
if len(rv.relinked) != 1 || rv.relinked[0] != tid {
|
||||
t.Errorf("relinked = %v, want [%s]", rv.relinked, tid)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -696,10 +706,10 @@ func TestRerecognize(t *testing.T) {
|
||||
Reader: &fakeReader{}, Reviewer: rv})
|
||||
cl := noRedirectClient()
|
||||
|
||||
if _, err := cl.Post(srv.URL+"/ui/downloads/1/rerecognize", "", nil); err != nil {
|
||||
if _, err := cl.Post(srv.URL+"/ui/downloads/"+tid+"/rerecognize", "", nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(rv.rerecognized) != 1 || rv.rerecognized[0] != 1 {
|
||||
t.Errorf("rerecognized = %v, want [1]", rv.rerecognized)
|
||||
if len(rv.rerecognized) != 1 || rv.rerecognized[0] != tid {
|
||||
t.Errorf("rerecognized = %v, want [%s]", rv.rerecognized, tid)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user