The free-text 'channel' filter was dead: it exact-matched SummaryRow.Channel,
which is just the provider ('youtube'), because videos never stored their source
channel. Now they do.
- migration 014: videos.channel_title (nullable; existing rows backfill on the
next discovery pass, pasted videos immediately).
- discovery (NewVideos) + paste (VideoByID) populate channel_title; UpsertVideo
persists it, preserving an existing title when an update arrives empty.
- store.DistinctChannels lists a user's channels (RLS-scoped); SummaryRow carries
ChannelTitle via the shared projection.
- Filter: single Channel -> Channels []string, matching on ChannelTitle; the feed
renders a multi-select of DistinctChannels (hidden until channels exist).
- migrate tests: 014 reversibility + fixed the relative-step counts in the 010/011
up/down tests (014 shifted the topology).
TDD throughout: channel persist + distinct, adapter channel wiring, multi-channel
filter match, handler channel filter, migration up/down.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
63 lines
1.9 KiB
Go
63 lines
1.9 KiB
Go
package youtube
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"testing"
|
|
|
|
"gitea.d-ma.be/mathias/tapir/internal/domain"
|
|
)
|
|
|
|
func TestVideoByID(t *testing.T) {
|
|
const id = "dQw4w9WgXcQ"
|
|
a, secrets := newTestAdapter(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/videos" {
|
|
t.Errorf("unexpected path %q (must use videos.list)", r.URL.Path)
|
|
}
|
|
if got := r.URL.Query().Get("id"); got != id {
|
|
t.Errorf("expected id=%s, got %q", id, got)
|
|
}
|
|
if got := r.URL.Query().Get("part"); got != "snippet" {
|
|
t.Errorf("expected part=snippet, got %q", got)
|
|
}
|
|
_, _ = w.Write([]byte(`{"items":[{"snippet":{"title":"Never Gonna Give You Up","channelTitle":"Rick Astley","publishedAt":"2026-05-20T09:00:00Z"}}]}`))
|
|
})
|
|
|
|
v, err := a.VideoByID(context.Background(), "u1", id)
|
|
if err != nil {
|
|
t.Fatalf("VideoByID: %v", err)
|
|
}
|
|
if v.UserID != "u1" {
|
|
t.Errorf("UserID = %q, want u1", v.UserID)
|
|
}
|
|
if v.ProviderVideoID != id || v.Title != "Never Gonna Give You Up" {
|
|
t.Errorf("unexpected video: %+v", v)
|
|
}
|
|
if v.ChannelTitle != "Rick Astley" {
|
|
t.Errorf("ChannelTitle = %q, want Rick Astley", v.ChannelTitle)
|
|
}
|
|
if v.Provider != domain.ProviderYouTube || v.URL != "https://www.youtube.com/watch?v="+id {
|
|
t.Errorf("video not wired correctly: %+v", v)
|
|
}
|
|
if v.PublishedAt.IsZero() {
|
|
t.Errorf("expected publishedAt parsed, got zero")
|
|
}
|
|
if v.SubscriptionID != "" {
|
|
t.Errorf("a pasted video must have no subscription, got %q", v.SubscriptionID)
|
|
}
|
|
if secrets.byRef == nil {
|
|
t.Errorf("token must be resolved by reference through the SecretStore")
|
|
}
|
|
}
|
|
|
|
func TestVideoByIDNotFound(t *testing.T) {
|
|
a, _ := newTestAdapter(t, func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte(`{"items":[]}`))
|
|
})
|
|
_, err := a.VideoByID(context.Background(), "u1", "missingvid0")
|
|
if !errors.Is(err, domain.ErrVideoNotFound) {
|
|
t.Fatalf("VideoByID for missing id = %v, want domain.ErrVideoNotFound", err)
|
|
}
|
|
}
|