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) } }