Release Please / Release Please (push) Skipped
Release / GoReleaser (push) Skipped
Tests / Secret scan (gitleaks) (push) Skipped
Tests / Test (Go 1.25) (push) Skipped
Tests / Test (Go stable) (push) Skipped
Tests / Secret scan (gitleaks) (pull_request) Successful in 4s
Tests / Test (Go 1.25) (pull_request) Successful in 59s
Tests / Test (Go stable) (pull_request) Successful in 1m7s
Showroom-safe: the tree no longer carries internal hosts, IPs, ports, personal names, client domains or client file names. Behaviour is unchanged and now supplied per deployment. - catalog: hardcoded mail-org / project classifiers become a YAML-driven Classifier (OO_CATALOG_CONFIG or --config); neutral default classifies nothing as work. New catalog/classify.go + example + tests. - storage_fallback: drop the baked-in MinIO endpoint IP; require MINIO_ENDPOINT (+ keys) from the env. - kontolink: build DocEditor links from $ONLYOFFICE_URL instead of a hardcoded portal host; kontoblatt: no client file id in the output name. - oo: load .env CLI-wide (bootstrap.LoadEnv in execute) so non-authenticating commands (catalog scans) also see config. - genericize comments/docs/fixtures (AGENTS, README, .env.example, crm-associations, catalog tests, ES/pdfattach tests, mails).
276 lines
8.2 KiB
Go
276 lines
8.2 KiB
Go
package onlyoffice
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestESTextSearchRequestShape(t *testing.T) {
|
|
got := esTextSearchRequest(SearchQuery{
|
|
Text: "S1021",
|
|
FolderID: "634",
|
|
Extensions: []string{".PDF", "pdf"},
|
|
Limit: 5,
|
|
})
|
|
if got.Size != 5 {
|
|
t.Errorf("size = %d, want 5", got.Size)
|
|
}
|
|
mm := got.Query.Bool.Must[0].MultiMatch
|
|
if mm == nil || !reflect.DeepEqual(mm.Fields, []string{"title^2", "content"}) {
|
|
t.Fatalf("multi_match = %+v, want title^2 + content", mm)
|
|
}
|
|
if _, ok := got.Highlight.Fields["content"]; !ok {
|
|
t.Error("content highlight missing")
|
|
}
|
|
if _, ok := got.Highlight.Fields["title"]; !ok {
|
|
t.Error("title highlight missing")
|
|
}
|
|
var folder, exts int
|
|
for _, f := range got.Query.Bool.Filter {
|
|
switch {
|
|
case f.Term != nil && f.Term["folder"] != nil:
|
|
folder++
|
|
if f.Term["folder"] != "634" {
|
|
t.Errorf("folder term = %+v", f.Term)
|
|
}
|
|
case f.Terms != nil:
|
|
exts++
|
|
if !reflect.DeepEqual(f.Terms["ext"], []string{"pdf"}) {
|
|
t.Errorf("ext terms = %+v, want deduped pdf", f.Terms)
|
|
}
|
|
}
|
|
}
|
|
if folder != 1 || exts != 1 {
|
|
t.Errorf("filters folder=%d exts=%d, want 1 each", folder, exts)
|
|
}
|
|
}
|
|
|
|
func TestESTextBulkBody(t *testing.T) {
|
|
body := esTextBulkBody([]TextDoc{
|
|
{ID: "3578", Title: "S1021.pdf", FolderID: "634", Ext: "pdf", Content: "Begleitzettel <S1021> & mehr"},
|
|
{ID: "3579", Title: "S1023.pdf", Ext: "pdf", Content: "x"},
|
|
})
|
|
lines := strings.Split(strings.TrimRight(string(body), "\n"), "\n")
|
|
if len(lines) != 4 {
|
|
t.Fatalf("bulk body has %d lines, want 4:\n%s", len(lines), body)
|
|
}
|
|
if !strings.Contains(lines[0], `"index"`) || !strings.Contains(lines[0], `"_id":"3578"`) {
|
|
t.Errorf("action line = %q", lines[0])
|
|
}
|
|
if !strings.Contains(lines[1], `"content":"Begleitzettel <S1021> & mehr"`) {
|
|
t.Errorf("source line should keep HTML unescaped, got %q", lines[1])
|
|
}
|
|
if !strings.Contains(lines[2], `"_id":"3579"`) {
|
|
t.Errorf("second action line = %q", lines[2])
|
|
}
|
|
}
|
|
|
|
func TestParseESTextResponse(t *testing.T) {
|
|
raw := []byte(`{
|
|
"hits": {
|
|
"total": {"value": 1, "relation": "eq"},
|
|
"hits": [
|
|
{
|
|
"_id": "3578",
|
|
"_score": 3.21,
|
|
"_source": {"id": "3578", "title": "2026-07-28-S1021-acme-rechnung.pdf", "folder": "634", "ext": "pdf"},
|
|
"highlight": {"content": ["Begleitzettel … <em>S1021</em> …"]}
|
|
}
|
|
]
|
|
}
|
|
}`)
|
|
hits, err := parseESTextResponse(raw)
|
|
if err != nil {
|
|
t.Fatalf("parseESTextResponse: %v", err)
|
|
}
|
|
if len(hits) != 1 {
|
|
t.Fatalf("hits = %d, want 1", len(hits))
|
|
}
|
|
h := hits[0]
|
|
if h.ID != "3578" || h.Title != "2026-07-28-S1021-acme-rechnung.pdf" || h.Kind != File {
|
|
t.Errorf("entry = %+v", h.Entry)
|
|
}
|
|
if h.ParentID != "634" || !reflect.DeepEqual(h.Path, []string{"634"}) {
|
|
t.Errorf("path = %v parent = %q", h.Path, h.ParentID)
|
|
}
|
|
if h.Provider != "es-text" {
|
|
t.Errorf("provider = %q", h.Provider)
|
|
}
|
|
if h.Highlight != "Begleitzettel … S1021 …" {
|
|
t.Errorf("highlight = %q, want tags stripped", h.Highlight)
|
|
}
|
|
}
|
|
|
|
func TestNewESTextIndexDefaults(t *testing.T) {
|
|
if _, err := NewESTextIndex(ESTextConfig{}); err == nil {
|
|
t.Error("empty URL: want error")
|
|
}
|
|
x, err := NewESTextIndex(ESTextConfig{URL: "http://es:9200/"})
|
|
if err != nil {
|
|
t.Fatalf("NewESTextIndex: %v", err)
|
|
}
|
|
if x.Index() != defaultESTextIndex {
|
|
t.Errorf("index = %q, want %q", x.Index(), defaultESTextIndex)
|
|
}
|
|
if x.cfg.URL != "http://es:9200" {
|
|
t.Errorf("url = %q, want trimmed", x.cfg.URL)
|
|
}
|
|
if x.Name() != "es-text" {
|
|
t.Errorf("Name() = %q", x.Name())
|
|
}
|
|
}
|
|
|
|
func TestESTextConfigFromEnvIndexDefault(t *testing.T) {
|
|
t.Setenv("ONLYOFFICE_ES_URL", "http://es:9200/")
|
|
t.Setenv("ONLYOFFICE_ES_TEXT_INDEX", "")
|
|
cfg := ESTextConfigFromEnv()
|
|
if cfg.Index != defaultESTextIndex {
|
|
t.Errorf("index = %q, want %q", cfg.Index, defaultESTextIndex)
|
|
}
|
|
}
|
|
|
|
func TestTextIndexerIndexEntries(t *testing.T) {
|
|
store := &textFakeStore{
|
|
files: map[string][]byte{"1": []byte("PDFBYTES")},
|
|
}
|
|
idx := &textFakeIndex{}
|
|
ix := NewTextIndexer(store, idx)
|
|
ix.Extractor = textFakeExtractor{prefix: "TEXT "}
|
|
|
|
res, err := ix.IndexEntries(context.Background(), []Entry{
|
|
{ID: "1", Title: "Rechnung.PDF", ParentID: "649", Kind: File},
|
|
{ID: "2", Title: "Tabelle.xlsx", ParentID: "649", Kind: File},
|
|
{ID: "3", Title: "Unterordner", Kind: Folder},
|
|
}, IndexOptions{})
|
|
if err != nil {
|
|
t.Fatalf("IndexEntries: %v", err)
|
|
}
|
|
if res.Scanned != 3 || res.Indexed != 1 || res.Skipped != 2 || res.Failed != 0 {
|
|
t.Errorf("result = %+v, want scanned=3 indexed=1 skipped=2 failed=0", res)
|
|
}
|
|
if len(idx.docs) != 1 {
|
|
t.Fatalf("indexed docs = %d, want 1", len(idx.docs))
|
|
}
|
|
got := idx.docs[0]
|
|
want := TextDoc{ID: "1", Title: "Rechnung.PDF", FolderID: "649", Ext: "pdf", Content: "TEXT PDFBYTES"}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Errorf("doc = %+v, want %+v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestTextIndexerRecordsExtractionFailure(t *testing.T) {
|
|
store := &textFakeStore{files: map[string][]byte{"1": []byte("x")}}
|
|
idx := &textFakeIndex{}
|
|
ix := NewTextIndexer(store, idx)
|
|
ix.Extractor = textFailingExtractor{}
|
|
|
|
res, err := ix.IndexEntries(context.Background(), []Entry{{ID: "1", Title: "a.pdf", Kind: File}}, IndexOptions{})
|
|
if err != nil {
|
|
t.Fatalf("IndexEntries: %v", err)
|
|
}
|
|
if res.Indexed != 0 || res.Failed != 1 || len(res.Errors) != 1 {
|
|
t.Errorf("result = %+v, want one failure recorded", res)
|
|
}
|
|
}
|
|
|
|
func TestTextIndexerPlanFolder(t *testing.T) {
|
|
store := &textFakeStore{dirs: map[string][]Entry{
|
|
"root": {
|
|
{ID: "10", Title: "a.pdf", Kind: File},
|
|
{ID: "11", Title: "sub", Kind: Folder},
|
|
},
|
|
"11": {
|
|
{ID: "12", Title: "b.PDF", Kind: File},
|
|
{ID: "13", Title: "c.xlsx", Kind: File},
|
|
},
|
|
}}
|
|
ix := NewTextIndexer(store, &textFakeIndex{})
|
|
|
|
flat, err := ix.PlanFolder(context.Background(), "root", IndexOptions{})
|
|
if err != nil {
|
|
t.Fatalf("PlanFolder: %v", err)
|
|
}
|
|
if len(flat) != 1 || flat[0].ID != "10" {
|
|
t.Errorf("flat plan = %+v, want only a.pdf", flat)
|
|
}
|
|
deep, err := ix.PlanFolder(context.Background(), "root", IndexOptions{Recursive: true, Limit: 10})
|
|
if err != nil {
|
|
t.Fatalf("PlanFolder recursive: %v", err)
|
|
}
|
|
if len(deep) != 2 {
|
|
t.Errorf("recursive plan = %d entries, want 2", len(deep))
|
|
}
|
|
}
|
|
|
|
// --- fakes -----------------------------------------------------------------
|
|
|
|
type textFakeStore struct {
|
|
dirs map[string][]Entry
|
|
files map[string][]byte
|
|
stat map[string]Entry
|
|
}
|
|
|
|
func (f *textFakeStore) Name() string { return "fake" }
|
|
|
|
func (f *textFakeStore) List(_ context.Context, parentID string) ([]Entry, error) {
|
|
return f.dirs[parentID], nil
|
|
}
|
|
|
|
func (f *textFakeStore) Stat(_ context.Context, id string) (Entry, error) {
|
|
if e, ok := f.stat[id]; ok {
|
|
return e, nil
|
|
}
|
|
return Entry{}, fmt.Errorf("not found: %s", id)
|
|
}
|
|
|
|
func (f *textFakeStore) Download(_ context.Context, id string, w io.Writer) (int64, error) {
|
|
b, ok := f.files[id]
|
|
if !ok {
|
|
return 0, fmt.Errorf("no bytes for %s", id)
|
|
}
|
|
n, err := w.Write(b)
|
|
return int64(n), err
|
|
}
|
|
|
|
func (f *textFakeStore) CreateFolder(context.Context, string, string) (Entry, error) {
|
|
return Entry{}, nil
|
|
}
|
|
func (f *textFakeStore) Upload(context.Context, string, string, io.Reader) (Entry, error) {
|
|
return Entry{}, nil
|
|
}
|
|
func (f *textFakeStore) Move(context.Context, []string, string) error { return nil }
|
|
func (f *textFakeStore) Copy(context.Context, []string, string) error { return nil }
|
|
func (f *textFakeStore) Rename(context.Context, string, string) error { return nil }
|
|
func (f *textFakeStore) Delete(context.Context, []string) error { return nil }
|
|
|
|
type textFakeIndex struct{ docs []TextDoc }
|
|
|
|
func (f *textFakeIndex) Put(_ context.Context, docs []TextDoc) error {
|
|
f.docs = append(f.docs, docs...)
|
|
return nil
|
|
}
|
|
func (f *textFakeIndex) Delete(context.Context, []string) error { return nil }
|
|
func (f *textFakeIndex) Search(context.Context, SearchQuery) ([]SearchHit, error) { return nil, nil }
|
|
func (f *textFakeIndex) Name() string { return "fake" }
|
|
|
|
type textFakeExtractor struct{ prefix string }
|
|
|
|
func (f textFakeExtractor) Extract(path, _, _ string, _ int) (string, error) {
|
|
b, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return f.prefix + string(b), nil
|
|
}
|
|
|
|
type textFailingExtractor struct{}
|
|
|
|
func (textFailingExtractor) Extract(string, string, string, int) (string, error) {
|
|
return "", fmt.Errorf("boom")
|
|
}
|