Infra ADR-0004 renamed the Gitea host. Bulk replace across go.mod and all .go import paths. Build and tests pass unchanged. Closes #20 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dt6aHEDWRjkK14Voi6HnGh
172 lines
5.9 KiB
Go
172 lines
5.9 KiB
Go
package web_test
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"git.d-ma.be/mathias/tapir/internal/web"
|
|
)
|
|
|
|
// fakeProcessor records ProcessVideo calls. With block set it parks until the
|
|
// channel is closed, so a test can observe the handler return before the
|
|
// background work finishes (proving it ran in a goroutine).
|
|
type fakeProcessor struct {
|
|
block chan struct{}
|
|
done chan struct{}
|
|
calls []string
|
|
}
|
|
|
|
func (f *fakeProcessor) ProcessVideo(_ context.Context, _, videoID string) error {
|
|
if f.block != nil {
|
|
<-f.block
|
|
}
|
|
f.calls = append(f.calls, videoID)
|
|
if f.done != nil {
|
|
close(f.done)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestRequestSummarizeImmediateProcessing(t *testing.T) {
|
|
app := newApp(t)
|
|
p := rawPool(t)
|
|
resetDB(t, p)
|
|
seedVideo(t, p, videoX, "Pending Title", "https://x", time.Time{})
|
|
|
|
fp := &fakeProcessor{block: make(chan struct{}), done: make(chan struct{})}
|
|
app.Processor = fp
|
|
|
|
rec := postSummarize(t, app, videoX, true)
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
html := body(t, rec)
|
|
|
|
// The processing card came back while ProcessVideo is still parked on block:
|
|
// the work runs in a goroutine, the handler did not wait for it.
|
|
require.Contains(t, html, "Summarizing", "processing card returned")
|
|
require.Contains(t, html, "╭", "charm box rendered")
|
|
require.Contains(t, html, "▓", "tapir body block chars rendered")
|
|
require.Contains(t, html, "∩", "wiggling snout frame rendered")
|
|
require.Contains(t, html, web.CharmPurple, "charm palette applied to the border")
|
|
require.Contains(t, html, "/v/"+videoX+"/status", "card polls the status endpoint")
|
|
require.Contains(t, html, `hx-trigger="every 2s"`, "card auto-polls every 2s")
|
|
require.NotContains(t, html, "Queued", "not the queue-only card")
|
|
|
|
close(fp.block)
|
|
select {
|
|
case <-fp.done:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("ProcessVideo was not called in the background")
|
|
}
|
|
require.Equal(t, []string{videoX}, fp.calls)
|
|
}
|
|
|
|
func TestStatusProcessingThenDone(t *testing.T) {
|
|
ctx := context.Background()
|
|
app := newApp(t)
|
|
p := rawPool(t)
|
|
resetDB(t, p)
|
|
seedVideo(t, p, videoX, "Pending Title", "https://x", time.Time{})
|
|
|
|
// Park ProcessVideo so the video stays in-flight while we poll status.
|
|
fp := &fakeProcessor{block: make(chan struct{})}
|
|
app.Processor = fp
|
|
require.Equal(t, http.StatusOK, postSummarize(t, app, videoX, true).Code)
|
|
|
|
// Processing: status returns the animation card, still polling.
|
|
rec := getStatus(t, app, videoX)
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
html := body(t, rec)
|
|
require.Contains(t, html, "Summarizing", "in-flight → animation card")
|
|
require.Contains(t, html, `hx-trigger="every 2s"`, "still polling")
|
|
|
|
close(fp.block)
|
|
|
|
// Done: once a summary exists, status returns the summary card with no poll.
|
|
require.NoError(t, deliver(ctx, app, videoX, "the summary body"))
|
|
rec = getStatus(t, app, videoX)
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
html = body(t, rec)
|
|
require.NotContains(t, html, "Summarizing", "done → no animation")
|
|
require.NotContains(t, html, "every 2s", "done card does not poll (polling stops)")
|
|
require.Contains(t, html, "/v/"+videoX+"\"", "links to the detail page")
|
|
}
|
|
|
|
func TestStatusQueuedWhenNotInFlight(t *testing.T) {
|
|
ctx := context.Background()
|
|
app := newApp(t)
|
|
p := rawPool(t)
|
|
resetDB(t, p)
|
|
seedVideo(t, p, videoX, "Pending Title", "https://x", time.Time{})
|
|
|
|
// Flag set but nothing in-flight (e.g. queue-only, or after a restart).
|
|
require.NoError(t, app.Store.RequestSummarize(ctx, userID, videoX))
|
|
|
|
rec := getStatus(t, app, videoX)
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
html := body(t, rec)
|
|
require.Contains(t, html, "Queued", "queued chip card")
|
|
require.NotContains(t, html, "Summarizing", "not processing")
|
|
require.NotContains(t, html, "every 2s", "queued card does not poll")
|
|
}
|
|
|
|
func TestStatusNotFound(t *testing.T) {
|
|
app := newApp(t)
|
|
resetDB(t, rawPool(t))
|
|
rec := getStatus(t, app, videoX)
|
|
require.Equal(t, http.StatusNotFound, rec.Code)
|
|
}
|
|
|
|
func getStatus(t *testing.T, app *web.App, videoID string) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
req := httptest.NewRequest(http.MethodGet, "/v/"+videoID+"/status", nil)
|
|
req.Header.Set("HX-Request", "true")
|
|
rec := httptest.NewRecorder()
|
|
app.Router().ServeHTTP(rec, req)
|
|
return rec
|
|
}
|
|
|
|
// setTranscriptStatus stamps videos.transcript_status directly (bypassing RLS via
|
|
// the super pool) so a test can drive the status endpoint into a given state.
|
|
func setTranscriptStatus(t *testing.T, p *pgxpool.Pool, videoID, status string) {
|
|
t.Helper()
|
|
_, err := p.Exec(context.Background(),
|
|
`UPDATE videos SET transcript_status = $2 WHERE id = $1`, videoID, status)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// TestStatusRateLimitedShowsWaitingCard: a click that hit YouTube's rate limit
|
|
// must surface the honest "waiting, will retry" card that keeps polling — not a
|
|
// silent revert to the Summarize button.
|
|
func TestStatusRateLimitedShowsWaitingCard(t *testing.T) {
|
|
app := newApp(t)
|
|
p := rawPool(t)
|
|
resetDB(t, p)
|
|
seedVideo(t, p, videoX, "Throttled Title", "https://x", time.Time{})
|
|
setTranscriptStatus(t, p, videoX, "rate_limited")
|
|
|
|
html := body(t, getStatus(t, app, videoX))
|
|
require.Contains(t, html, "Waiting on YouTube rate limits", "honest rate-limit copy")
|
|
require.Contains(t, html, `hx-trigger="every 30s"`, "waiting card keeps polling so it self-resolves")
|
|
require.NotContains(t, html, "Summarize this video", "must not revert to the Summarize button")
|
|
}
|
|
|
|
// TestStatusNoCaptionsTerminal: a no-captions outcome is terminal — an honest
|
|
// message, no poll, no button to click back into the same dead end.
|
|
func TestStatusNoCaptionsTerminal(t *testing.T) {
|
|
app := newApp(t)
|
|
p := rawPool(t)
|
|
resetDB(t, p)
|
|
seedVideo(t, p, videoX, "Silent Title", "https://x", time.Time{})
|
|
setTranscriptStatus(t, p, videoX, "none")
|
|
|
|
html := body(t, getStatus(t, app, videoX))
|
|
require.Contains(t, html, "No captions available", "honest terminal copy")
|
|
require.NotContains(t, html, "hx-trigger", "terminal card must stop polling")
|
|
}
|