Files
sm-bm-plugin/console/materials_reindex_test.go
Jakub Zych f841e8ed71 feat: extract file text on every attach path and backfill on reindex
- inline and signed uploads queue the text job in their transaction
- the admin form queues it in the save transaction
- detach resyncs the material (its text row goes by cascade)
- bm:materials-reindex backfills missing PDF texts before each sync
2026-11-08 20:16:14 +01:00

172 lines
6.0 KiB
Go

package console_test
import (
"bytes"
"context"
"errors"
"net/http"
"slices"
"strconv"
"strings"
"testing"
"git.golem15.com/golem15/summercms/modules/bonfire"
"git.golem15.com/golem15/summercms/modules/lagoon"
"git.golem15.com/jakub/sm-bm-plugin/classes"
"git.golem15.com/jakub/sm-bm-plugin/console"
"git.golem15.com/jakub/sm-bm-plugin/internal/searchtest"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
func TestMaterialsReindexRefuses(t *testing.T) {
asked := false
deps := console.MaterialsReindexDeps{
DB: func(context.Context) (*gorm.DB, func(), error) {
asked = true
return nil, func() {}, nil
},
Index: classes.NewMaterialIndexer(searchtest.App(t)),
}
cmds := []bonfire.Command{console.MaterialsReindexCommand(deps)}
var out bytes.Buffer
err := bonfire.Call(context.Background(), cmds, console.MaterialsReindexName, nil, &out)
if err == nil || !strings.Contains(err.Error(), "search.driver") || !strings.Contains(err.Error(), "search.typesense.api_key") {
t.Errorf("unconfigured engine: err = %v, want the configuration hint", err)
}
out.Reset()
if err := bonfire.Call(context.Background(), cmds, console.MaterialsReindexName, []string{"--if-configured"}, &out); err != nil {
t.Errorf("--if-configured: %v", err)
}
if !strings.Contains(out.String(), "not configured") {
t.Errorf("--if-configured output %q, want the notice", out.String())
}
if asked {
t.Error("the command opened the database without an engine")
}
}
func TestMaterialsReindexFresh(t *testing.T) {
stub := &searchtest.Stub{}
app := searchtest.App(t, searchtest.Start(t, stub)...)
gdb, err := gorm.Open(postgres.New(postgres.Config{DSN: "host=127.0.0.1"}), &gorm.Config{DryRun: true, DisableAutomaticPing: true})
if err != nil {
t.Fatal(err)
}
sqlDB, err := gdb.DB()
if err != nil {
t.Fatal(err)
}
if err := lagoon.Publish(app, sqlDB, gdb); err != nil {
t.Fatal(err)
}
deps := console.MaterialsReindexDeps{
DB: func(context.Context) (*gorm.DB, func(), error) { return gdb, func() {}, nil },
Index: classes.NewMaterialIndexer(app),
}
cmds := []bonfire.Command{console.MaterialsReindexCommand(deps)}
coll := "/collections/" + searchtest.Index
var out bytes.Buffer
for run := 1; run <= 2; run++ {
before := len(stub.Requests())
out.Reset()
if err := bonfire.Call(context.Background(), cmds, console.MaterialsReindexName, []string{"--fresh"}, &out); err != nil {
t.Fatalf("run %d: %v\n%s", run, err, out.String())
}
var sent []string
for _, r := range stub.Requests()[before:] {
sent = append(sent, r.Method+" "+r.Path)
}
if want := []string{http.MethodDelete + " " + coll, http.MethodGet + " " + coll, http.MethodPost + " /collections"}; !slices.Equal(sent, want) {
t.Errorf("run %d sent %v, want %v", run, sent, want)
}
if !strings.Contains(out.String(), "Reindexed: 0 synced, 0 failed") {
t.Errorf("run %d output %q", run, out.String())
}
}
// The second run dropped the collection the first one created.
if !strings.Contains(out.String(), "Dropped "+searchtest.Index) {
t.Errorf("second run output %q, want Dropped", out.String())
}
}
func TestMaterialsReindexBackfill(t *testing.T) {
ctx := context.Background()
var calls []string
var synced []uint
sync := func(id uint) error {
calls = append(calls, "sync "+strconv.FormatUint(uint64(id), 10))
synced = append(synced, id)
return nil
}
// pdftotext missing on the first material: one warning, no more
// backfill calls, and every material is still synced.
deps := console.MaterialsReindexDeps{Backfill: func(_ context.Context, _ *gorm.DB, id uint) error {
calls = append(calls, "backfill "+strconv.FormatUint(uint64(id), 10))
return classes.ErrPDFToolMissing
}}
var stdout, stderr bytes.Buffer
run := console.NewReindexRun(deps, sync, bonfire.NewOutput(strings.NewReader(""), &stdout, &stderr))
for _, id := range []uint{1, 2, 3} {
run.Material(ctx, nil, id)
}
if want := []string{"backfill 1", "sync 1", "sync 2", "sync 3"}; !slices.Equal(calls, want) {
t.Errorf("calls = %v, want %v", calls, want)
}
if !slices.Equal(synced, []uint{1, 2, 3}) {
t.Errorf("synced = %v, want all three", synced)
}
all := stdout.String() + stderr.String()
if n := strings.Count(all, "pdftotext not found"); n != 1 {
t.Errorf("the tool warning was printed %d times, want once:\n%s", n, all)
}
if !strings.Contains(all, "install poppler-utils") {
t.Errorf("output %q, want the poppler-utils hint", all)
}
if s, f, noTool := run.Counts(); s != 3 || f != 0 || !noTool {
t.Errorf("counts = %d synced %d failed noTool %v, want 3 0 true", s, f, noTool)
}
// Another backfill error warns for that material only; it is still
// synced and the next material is backfilled. A failed sync counts.
calls, synced = nil, nil
stdout.Reset()
stderr.Reset()
deps.Backfill = func(_ context.Context, _ *gorm.DB, id uint) error {
calls = append(calls, "backfill "+strconv.FormatUint(uint64(id), 10))
if id == 1 {
return errors.New("blob gone")
}
return nil
}
failing := func(id uint) error {
if err := sync(id); err != nil || id == 3 {
return errors.New("engine down")
}
return nil
}
run = console.NewReindexRun(deps, failing, bonfire.NewOutput(strings.NewReader(""), &stdout, &stderr))
for _, id := range []uint{1, 2, 3} {
run.Material(ctx, nil, id)
}
if want := []string{"backfill 1", "sync 1", "backfill 2", "sync 2", "backfill 3", "sync 3"}; !slices.Equal(calls, want) {
t.Errorf("calls = %v, want %v", calls, want)
}
all = stdout.String() + stderr.String()
if !strings.Contains(all, "material 1: file text: blob gone") || strings.Contains(all, "pdftotext not found") {
t.Errorf("output %q, want the material 1 file text warning only", all)
}
if s, f, noTool := run.Counts(); s != 2 || f != 1 || noTool {
t.Errorf("counts = %d synced %d failed noTool %v, want 2 1 false", s, f, noTool)
}
// No Backfill: only syncs.
calls = nil
run = console.NewReindexRun(console.MaterialsReindexDeps{}, sync, bonfire.NewOutput(strings.NewReader(""), &stdout, &stderr))
run.Material(ctx, nil, 9)
if !slices.Equal(calls, []string{"sync 9"}) {
t.Errorf("calls without Backfill = %v", calls)
}
}