A push to main and its version tag fire two CI runs for the same commit. Both ran `go test ./...`, which starts embedded-postgres on a FIXED port (54329/54330) and a shared data dir. -p 1 serialises packages WITHIN a run, not across two concurrent runs — so when the two runs overlapped they collided on the port/data dir and BOTH failed the Lint/Test job (no image built). Prior commits passed only because their two runs happened not to overlap. Derive the port and runtime/data dirs from the PID; share only CachePath so the PG archive downloads once. Proven: two concurrent `go test` of the store package now both pass. Unblocks the v0.21.0 (Pillar A) build. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
527 lines
19 KiB
Go
527 lines
19 KiB
Go
package web_test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
embeddedpostgres "github.com/fergusstrange/embedded-postgres"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"gitea.d-ma.be/mathias/tapir/internal/adapters/store"
|
|
"gitea.d-ma.be/mathias/tapir/internal/domain"
|
|
"gitea.d-ma.be/mathias/tapir/internal/web"
|
|
)
|
|
|
|
// dsn points at the in-process Postgres started in TestMain. Handler tests run
|
|
// against the real store (real SQL, the lane-A actions) behind StubAuth — the
|
|
// full read/write path minus live Dex.
|
|
var dsn string
|
|
|
|
func TestMain(m *testing.M) {
|
|
// Per-process port + dirs so concurrent `go test` runs (e.g. a push-run and a
|
|
// tag-run in CI) never collide on a fixed port or shared data dir. Base 55000
|
|
// keeps web's range distinct from the store package (54000). Shared CachePath
|
|
// downloads the PG archive once.
|
|
port := uint32(55000 + os.Getpid()%1000)
|
|
dsn = fmt.Sprintf("postgres://postgres:postgres@localhost:%d/postgres?sslmode=disable", port)
|
|
|
|
rt := filepath.Join(os.TempDir(), fmt.Sprintf("tapir-epg-web-%d", os.Getpid()))
|
|
pg := embeddedpostgres.NewDatabase(
|
|
embeddedpostgres.DefaultConfig().
|
|
Port(port).
|
|
RuntimePath(rt).
|
|
DataPath(filepath.Join(rt, "data")).
|
|
BinariesPath(filepath.Join(rt, "bin")).
|
|
CachePath(filepath.Join(os.TempDir(), "tapir-epg-cache")),
|
|
)
|
|
if err := pg.Start(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "embedded-postgres start: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
code := m.Run()
|
|
if err := pg.Stop(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "embedded-postgres stop: %v\n", err)
|
|
}
|
|
_ = os.RemoveAll(rt)
|
|
os.Exit(code)
|
|
}
|
|
|
|
const (
|
|
userID = "11111111-1111-1111-1111-111111111111"
|
|
videoX = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
|
videoY = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"
|
|
// stubSubject is the StubAuth Dex subject the registration gate resolves to
|
|
// the fixed userID (mapping seeded by resetDB).
|
|
stubSubject = "stub-subject-xyz"
|
|
)
|
|
|
|
func newStore(t *testing.T) *store.Store {
|
|
t.Helper()
|
|
s, err := store.New(context.Background(), dsn)
|
|
require.NoError(t, err)
|
|
t.Cleanup(s.Close)
|
|
return s
|
|
}
|
|
|
|
func rawPool(t *testing.T) *pgxpool.Pool {
|
|
t.Helper()
|
|
p, err := pgxpool.New(context.Background(), dsn)
|
|
require.NoError(t, err)
|
|
t.Cleanup(p.Close)
|
|
return p
|
|
}
|
|
|
|
// truncateAll wipes every table to a pristine state (user_identities is cleared
|
|
// via the ON DELETE CASCADE from users). Registration tests use this directly so
|
|
// no subject is pre-registered.
|
|
func truncateAll(t *testing.T, p *pgxpool.Pool) {
|
|
t.Helper()
|
|
_, err := p.Exec(context.Background(),
|
|
`TRUNCATE login_events, summary_actions, sink_deliveries, summaries, transcripts, videos, users CASCADE`)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// resetDB truncates, then seeds the StubAuth identity (stubSubject → userID) so
|
|
// the registration gate resolves the stub user and the existing handler tests can
|
|
// keep seeding and scoping by the fixed userID.
|
|
func resetDB(t *testing.T, p *pgxpool.Pool) {
|
|
t.Helper()
|
|
truncateAll(t, p)
|
|
ctx := context.Background()
|
|
_, err := p.Exec(ctx, `INSERT INTO users (id) VALUES ($1) ON CONFLICT (id) DO NOTHING`, userID)
|
|
require.NoError(t, err)
|
|
_, err = p.Exec(ctx,
|
|
`INSERT INTO user_identities (dex_subject, user_id) VALUES ($1, $2)
|
|
ON CONFLICT (dex_subject) DO NOTHING`, stubSubject, userID)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// newApp builds the App under test as the registered stub user (subject
|
|
// stubSubject, resolved to userID by resetDB). This is cmd/tapir's serve wiring
|
|
// minus Dex: the store is both the Store and the Identity port.
|
|
func newApp(t *testing.T) *web.App {
|
|
t.Helper()
|
|
return newAppAs(t, stubSubject)
|
|
}
|
|
|
|
// newAppAs builds the App under test with a specific StubAuth Dex subject, so
|
|
// registration-gate tests can drive registered vs unregistered subjects.
|
|
func newAppAs(t *testing.T, subject string) *web.App {
|
|
t.Helper()
|
|
s := newStore(t)
|
|
return &web.App{
|
|
Store: s,
|
|
Identity: s,
|
|
Auth: web.StubAuth{U: web.User{Subject: subject}},
|
|
}
|
|
}
|
|
|
|
func summary(videoID, text string) domain.Summary {
|
|
return domain.Summary{
|
|
UserID: userID,
|
|
VideoID: videoID,
|
|
Summary: text,
|
|
Highlights: []string{"highlight one", "highlight two"},
|
|
Takeaways: []string{"takeaway one"},
|
|
AIProvider: "local",
|
|
AIModel: "phi4-mini",
|
|
}
|
|
}
|
|
|
|
func seedVideo(t *testing.T, p *pgxpool.Pool, videoID, title, url string, published time.Time) {
|
|
t.Helper()
|
|
var pub any
|
|
if !published.IsZero() {
|
|
pub = published
|
|
}
|
|
_, err := p.Exec(context.Background(),
|
|
`INSERT INTO videos (id, user_id, provider, provider_video_id, title, url, published_at)
|
|
VALUES ($1, $2, 'youtube', $3, $4, $5, $6)`,
|
|
videoID, userID, "pv-"+videoID[:8], title, url, pub)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func do(t *testing.T, app *web.App, req *http.Request) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
rec := httptest.NewRecorder()
|
|
app.Router().ServeHTTP(rec, req)
|
|
return rec
|
|
}
|
|
|
|
func body(t *testing.T, rec *httptest.ResponseRecorder) string {
|
|
t.Helper()
|
|
b, err := io.ReadAll(rec.Body)
|
|
require.NoError(t, err)
|
|
return string(b)
|
|
}
|
|
|
|
func TestHealthzNoAuth(t *testing.T) {
|
|
app := newApp(t)
|
|
rec := do(t, app, httptest.NewRequest(http.MethodGet, "/healthz", nil))
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
require.Equal(t, "ok", body(t, rec))
|
|
}
|
|
|
|
func TestListRendersRowsAndActionState(t *testing.T) {
|
|
ctx := context.Background()
|
|
app := newApp(t)
|
|
p := rawPool(t)
|
|
resetDB(t, p)
|
|
|
|
require.NoError(t, app.Store.SetAction(ctx, userID, videoX, "watched")) // action exists pre-summary
|
|
require.NoError(t, deliver(ctx, app, videoX, "body x"))
|
|
require.NoError(t, deliver(ctx, app, videoY, "body y"))
|
|
seedVideo(t, p, videoX, "X Title", "https://x", time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC))
|
|
seedVideo(t, p, videoY, "Y Title", "https://y", time.Time{})
|
|
|
|
rec := do(t, app, httptest.NewRequest(http.MethodGet, "/", nil))
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
html := body(t, rec)
|
|
|
|
require.Contains(t, html, "<html", "full page on a non-HTMX request")
|
|
require.Contains(t, html, "X Title")
|
|
require.Contains(t, html, "Y Title")
|
|
require.Contains(t, html, "youtube")
|
|
require.Contains(t, html, "watched", "current action state shown in the list")
|
|
require.Contains(t, html, "2026-01-01")
|
|
require.Contains(t, html, `class="card"`, "rows render as cards, not a table")
|
|
}
|
|
|
|
func TestListHTMXReturnsFragment(t *testing.T) {
|
|
ctx := context.Background()
|
|
app := newApp(t)
|
|
p := rawPool(t)
|
|
resetDB(t, p)
|
|
require.NoError(t, deliver(ctx, app, videoX, "body x"))
|
|
seedVideo(t, p, videoX, "X Title", "https://x", time.Time{})
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
req.Header.Set("HX-Request", "true")
|
|
rec := do(t, app, req)
|
|
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
html := body(t, rec)
|
|
require.Contains(t, html, `class="cards"`)
|
|
require.NotContains(t, html, "<html", "HTMX request gets only the list fragment")
|
|
}
|
|
|
|
func TestListChannelFilter(t *testing.T) {
|
|
ctx := context.Background()
|
|
app := newApp(t)
|
|
p := rawPool(t)
|
|
resetDB(t, p)
|
|
require.NoError(t, deliver(ctx, app, videoX, "body x"))
|
|
seedVideo(t, p, videoX, "X Title", "https://x", time.Time{})
|
|
_, err := p.Exec(ctx, `UPDATE videos SET channel_title = 'Acme Channel' WHERE id = $1`, videoX)
|
|
require.NoError(t, err)
|
|
|
|
// Selecting a different channel hides the row; selecting its channel shows it.
|
|
rec := do(t, app, httptest.NewRequest(http.MethodGet, "/?channel=Other+Channel", nil))
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
require.NotContains(t, body(t, rec), "X Title")
|
|
|
|
rec = do(t, app, httptest.NewRequest(http.MethodGet, "/?channel=Acme+Channel", nil))
|
|
require.Contains(t, body(t, rec), "X Title")
|
|
}
|
|
|
|
func TestDetailRendersHighlightsAndTakeaways(t *testing.T) {
|
|
ctx := context.Background()
|
|
app := newApp(t)
|
|
p := rawPool(t)
|
|
resetDB(t, p)
|
|
require.NoError(t, deliver(ctx, app, videoX, "the full summary body"))
|
|
seedVideo(t, p, videoX, "X Title", "https://x", time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC))
|
|
|
|
rec := do(t, app, httptest.NewRequest(http.MethodGet, "/v/"+videoX, nil))
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
html := body(t, rec)
|
|
|
|
require.Contains(t, html, "the full summary body")
|
|
require.Contains(t, html, "highlight one")
|
|
require.Contains(t, html, "takeaway one")
|
|
require.Contains(t, html, `id="action-buttons"`, "action button group present")
|
|
require.Contains(t, html, "Watched")
|
|
require.Contains(t, html, `class="segmented"`, "watched/skipped render as a segmented control (UX review C5)")
|
|
require.Less(t, strings.Index(html, "Skipped"), strings.Index(html, "Saved"),
|
|
"Saved sits after the watched/skipped segment")
|
|
require.Contains(t, html, "← Summaries", "back link to the list (UX review C3)")
|
|
|
|
// The detail page leads with the attention-saving payload (UX review A8):
|
|
// Takeaways ("is it worth my time?") above Highlights above the full Summary.
|
|
takeaways := strings.Index(html, "takeaway one")
|
|
highlights := strings.Index(html, "highlight one")
|
|
summary := strings.Index(html, "the full summary body")
|
|
require.Less(t, takeaways, highlights, "Takeaways render before Highlights")
|
|
require.Less(t, highlights, summary, "Highlights render before the full Summary")
|
|
}
|
|
|
|
func TestDetailNotFound(t *testing.T) {
|
|
app := newApp(t)
|
|
resetDB(t, rawPool(t))
|
|
rec := do(t, app, httptest.NewRequest(http.MethodGet, "/v/"+videoX, nil))
|
|
require.Equal(t, http.StatusNotFound, rec.Code)
|
|
}
|
|
|
|
func TestActionToggleReturnsFragment(t *testing.T) {
|
|
ctx := context.Background()
|
|
app := newApp(t)
|
|
resetDB(t, rawPool(t))
|
|
require.NoError(t, deliver(ctx, app, videoX, "body x"))
|
|
|
|
// First click: sets "watched", returns the fragment with it active.
|
|
rec := postAction(t, app, videoX, "watched", true)
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
html := body(t, rec)
|
|
require.Contains(t, html, `id="action-buttons"`)
|
|
require.Contains(t, html, "✓ Watched", "active verb marked")
|
|
|
|
got, err := app.Store.ActionsFor(ctx, userID, []string{videoX})
|
|
require.NoError(t, err)
|
|
require.Equal(t, map[string][]string{videoX: {"watched"}}, got)
|
|
|
|
// Second click on the same verb: clears it.
|
|
rec = postAction(t, app, videoX, "watched", true)
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
require.NotContains(t, body(t, rec), "✓ Watched", "re-click cleared the action")
|
|
|
|
got, err = app.Store.ActionsFor(ctx, userID, []string{videoX})
|
|
require.NoError(t, err)
|
|
require.Empty(t, got, "action cleared in the store")
|
|
}
|
|
|
|
func TestActionNonHTMXRedirects(t *testing.T) {
|
|
ctx := context.Background()
|
|
app := newApp(t)
|
|
resetDB(t, rawPool(t))
|
|
require.NoError(t, deliver(ctx, app, videoX, "body x"))
|
|
|
|
rec := postAction(t, app, videoX, "saved", false)
|
|
require.Equal(t, http.StatusSeeOther, rec.Code)
|
|
require.Equal(t, "/v/"+videoX, rec.Header().Get("Location"))
|
|
|
|
got, err := app.Store.ActionsFor(ctx, userID, []string{videoX})
|
|
require.NoError(t, err)
|
|
require.Equal(t, map[string][]string{videoX: {"saved"}}, got, "action persisted on the no-JS path")
|
|
}
|
|
|
|
func TestActionRejectsUnknownVerb(t *testing.T) {
|
|
app := newApp(t)
|
|
resetDB(t, rawPool(t))
|
|
rec := postAction(t, app, videoX, "bookmarked", true)
|
|
require.Equal(t, http.StatusBadRequest, rec.Code)
|
|
}
|
|
|
|
func TestListShowsSummarizeButtonForUnsummarized(t *testing.T) {
|
|
app := newApp(t)
|
|
p := rawPool(t)
|
|
resetDB(t, p)
|
|
// A discovered-but-unsummarized video (no summary delivered).
|
|
seedVideo(t, p, videoX, "Pending Title", "https://x", time.Time{})
|
|
|
|
rec := do(t, app, httptest.NewRequest(http.MethodGet, "/", nil))
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
html := body(t, rec)
|
|
|
|
require.Contains(t, html, "Pending Title", "unsummarized videos are listed too")
|
|
require.Contains(t, html, "Summarize", "a Summarize button is offered")
|
|
require.Contains(t, html, "/v/"+videoX+"/summarize", "button posts to the queue endpoint")
|
|
require.Contains(t, html, "card-pending", "muted pending treatment")
|
|
require.NotContains(t, html, "Queued", "not queued yet")
|
|
}
|
|
|
|
// TestListCollapsesOlderAndNoCaption verifies the feed IA (UX review B3/B4):
|
|
// summarized + recent un-summarized cards lead inline; older un-summarized
|
|
// videos collapse into a single disclosure; caption-less videos collapse into a
|
|
// one-line count instead of dead cards.
|
|
func TestListCollapsesOlderAndNoCaption(t *testing.T) {
|
|
ctx := context.Background()
|
|
app := newApp(t)
|
|
now := time.Date(2026, 6, 8, 12, 0, 0, 0, time.UTC)
|
|
app.RecencyWindow = 7 * 24 * time.Hour
|
|
app.Now = func() time.Time { return now }
|
|
p := rawPool(t)
|
|
resetDB(t, p)
|
|
|
|
const (
|
|
vRecent = "cccccccc-cccc-cccc-cccc-cccccccccccc" // 2d old, unsummarized → inline
|
|
vOld = "dddddddd-dddd-dddd-dddd-dddddddddddd" // 30d old, unsummarized → disclosure
|
|
vNoCap = "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee" // caption-less → collapsed line
|
|
)
|
|
require.NoError(t, deliver(ctx, app, videoX, "body x")) // summarized, recent
|
|
seedVideo(t, p, videoX, "Summarized X", "https://x", now.Add(-24*time.Hour))
|
|
seedVideo(t, p, vRecent, "Recent Pending", "https://r", now.Add(-2*24*time.Hour))
|
|
seedVideo(t, p, vOld, "Old Pending", "https://o", now.Add(-30*24*time.Hour))
|
|
seedVideo(t, p, vNoCap, "No Caption Vid", "https://n", now.Add(-40*24*time.Hour))
|
|
_, err := p.Exec(ctx, `UPDATE videos SET transcript_status = 'none' WHERE id = $1`, vNoCap)
|
|
require.NoError(t, err)
|
|
|
|
rec := do(t, app, httptest.NewRequest(http.MethodGet, "/", nil))
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
html := body(t, rec)
|
|
|
|
disclosure := strings.Index(html, "Show 1 older videos")
|
|
require.GreaterOrEqual(t, disclosure, 0, "older-videos disclosure present")
|
|
// Summarized + recent un-summarized lead inline, above the disclosure.
|
|
require.Less(t, strings.Index(html, "Summarized X"), disclosure, "summarized card is inline")
|
|
require.Less(t, strings.Index(html, "Recent Pending"), disclosure, "recent pending is inline")
|
|
// The older video is hidden inside the disclosure, after its summary.
|
|
require.Greater(t, strings.Index(html, "Old Pending"), disclosure, "older video lives in the disclosure")
|
|
// Caption-less video is a one-line count, never a card.
|
|
require.Contains(t, html, "have no captions")
|
|
require.NotContains(t, html, "No Caption Vid", "caption-less video is collapsed, not a card")
|
|
}
|
|
|
|
// TestListHidesFilterBarWhenEmpty: a genuinely empty list shows no filter bar
|
|
// (the connect CTA stands alone), but a filter that matches nothing still shows
|
|
// the bar so it can be cleared (UX review C1).
|
|
func TestListHidesFilterBarWhenEmpty(t *testing.T) {
|
|
app := newApp(t)
|
|
resetDB(t, rawPool(t))
|
|
|
|
rec := do(t, app, httptest.NewRequest(http.MethodGet, "/", nil))
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
require.NotContains(t, body(t, rec), `class="filters"`, "no filter bar on an empty account")
|
|
|
|
rec = do(t, app, httptest.NewRequest(http.MethodGet, "/?channel=nope", nil))
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
require.Contains(t, body(t, rec), `class="filters"`, "filtered-to-empty keeps the bar so it can be cleared")
|
|
}
|
|
|
|
func TestRequestSummarizeQueuesAndRendersCard(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{})
|
|
|
|
rec := postSummarize(t, app, videoX, true)
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
html := body(t, rec)
|
|
require.Contains(t, html, "Queued", "card now shows the queued state")
|
|
require.NotContains(t, html, ">Summarize<", "the Summarize button is gone once queued")
|
|
|
|
// The flag is persisted, so the next run picks it up.
|
|
row, err := app.Store.GetVideoRow(ctx, userID, videoX)
|
|
require.NoError(t, err)
|
|
require.True(t, row.SummarizeRequested)
|
|
}
|
|
|
|
func TestRequestSummarizeNonHTMXRedirects(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{})
|
|
|
|
rec := postSummarize(t, app, videoX, false)
|
|
require.Equal(t, http.StatusSeeOther, rec.Code)
|
|
require.Equal(t, "/", rec.Header().Get("Location"))
|
|
|
|
row, err := app.Store.GetVideoRow(ctx, userID, videoX)
|
|
require.NoError(t, err)
|
|
require.True(t, row.SummarizeRequested, "queued on the no-JS path too")
|
|
}
|
|
|
|
func TestRequestSummarizeNotFound(t *testing.T) {
|
|
app := newApp(t)
|
|
resetDB(t, rawPool(t))
|
|
rec := postSummarize(t, app, videoX, true)
|
|
require.Equal(t, http.StatusNotFound, rec.Code, "queuing an unknown video is a 404")
|
|
}
|
|
|
|
func TestSummarizeModeToggle(t *testing.T) {
|
|
ctx := context.Background()
|
|
app := newApp(t)
|
|
resetDB(t, rawPool(t))
|
|
|
|
// Account page defaults to automatic (ADR-018: onboarded users get
|
|
// zero-friction discovery — the list fills and summarizes itself).
|
|
rec := do(t, app, httptest.NewRequest(http.MethodGet, "/account", nil))
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
html := body(t, rec)
|
|
require.Contains(t, html, "Automatic", "default mode shown")
|
|
require.Contains(t, html, "Switch to manual")
|
|
|
|
// Toggle to manual via HTMX returns the refreshed control.
|
|
req := httptest.NewRequest(http.MethodPost, "/account/summarize-mode",
|
|
strings.NewReader("enabled=false"))
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
req.Header.Set("HX-Request", "true")
|
|
rec = do(t, app, req)
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
html = body(t, rec)
|
|
require.Contains(t, html, "Manual")
|
|
require.Contains(t, html, "Switch to automatic")
|
|
|
|
got, err := app.Store.GetAutoSummarize(ctx, userID)
|
|
require.NoError(t, err)
|
|
require.False(t, got, "mode persisted")
|
|
}
|
|
|
|
func postSummarize(t *testing.T, app *web.App, videoID string, htmx bool) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
req := httptest.NewRequest(http.MethodPost, "/v/"+videoID+"/summarize", nil)
|
|
if htmx {
|
|
req.Header.Set("HX-Request", "true")
|
|
}
|
|
return do(t, app, req)
|
|
}
|
|
|
|
// deliver stores a summary through the App's store under test.
|
|
func deliver(ctx context.Context, app *web.App, videoID, text string) error {
|
|
return app.Store.(*store.Store).Deliver(ctx, summary(videoID, text))
|
|
}
|
|
|
|
func postAction(t *testing.T, app *web.App, videoID, action string, htmx bool) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
req := httptest.NewRequest(http.MethodPost, "/v/"+videoID+"/action",
|
|
strings.NewReader("action="+action))
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
if htmx {
|
|
req.Header.Set("HX-Request", "true")
|
|
}
|
|
return do(t, app, req)
|
|
}
|
|
|
|
// TestListManualModeBannerCopy: a manual-mode user with un-summarized videos
|
|
// sees the manual prompt (click Summarize), NOT the "summaries land
|
|
// automatically" copy that misled the first pilot user into waiting forever.
|
|
func TestListManualModeBannerCopy(t *testing.T) {
|
|
ctx := context.Background()
|
|
app := newApp(t)
|
|
p := rawPool(t)
|
|
resetDB(t, p)
|
|
seedVideo(t, p, videoX, "X Title", "https://x", time.Time{}) // pending, un-summarized
|
|
require.NoError(t, app.Store.SetAutoSummarize(ctx, userID, false))
|
|
|
|
html := body(t, do(t, app, httptest.NewRequest(http.MethodGet, "/", nil)))
|
|
require.Contains(t, html, "Manual mode")
|
|
require.Contains(t, html, "are not summarized automatically")
|
|
require.NotContains(t, html, "land gradually",
|
|
"manual-mode user must not be told summaries arrive automatically")
|
|
}
|
|
|
|
// TestListAutoModeBannerCopy: an auto-mode user with a backlog sees the
|
|
// gradual-delivery copy, not the manual prompt.
|
|
func TestListAutoModeBannerCopy(t *testing.T) {
|
|
ctx := context.Background()
|
|
app := newApp(t)
|
|
p := rawPool(t)
|
|
resetDB(t, p)
|
|
seedVideo(t, p, videoX, "X Title", "https://x", time.Time{})
|
|
require.NoError(t, app.Store.SetAutoSummarize(ctx, userID, true))
|
|
|
|
html := body(t, do(t, app, httptest.NewRequest(http.MethodGet, "/", nil)))
|
|
require.Contains(t, html, "land gradually")
|
|
require.NotContains(t, html, "are not summarized automatically")
|
|
}
|