refactor(files): single file client facade + CLI/TUI migration (#38)
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 stable) (pull_request) Successful in 19s
Tests / Test (Go 1.25) (pull_request) Successful in 22s
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 stable) (pull_request) Successful in 19s
Tests / Test (Go 1.25) (pull_request) Successful in 22s
- FileClient composes FileStore/Searcher backends; Read()/Write()/Search() select REST/DAV/PG/ES with transient read fallback. Client.Files() returns it and *FileClient implements FileStore, so existing callers keep working. - Entry gains backend-native Updated + folder FilesCount/FoldersCount so dav ls output round-trips. - cmd/oo dav/projects files and cmd/office/fetch download/preview/delete go through FileStore; oo search goes through the facade. - Mark transport methods that FileStore now abstracts as deprecated.
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
package onlyoffice
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// fakeStore is a FileStore test double; it records which backend served a call
|
||||
// and returns a canned result or error.
|
||||
type fakeStore struct {
|
||||
name string
|
||||
entries []Entry
|
||||
err error
|
||||
calls *[]string
|
||||
}
|
||||
|
||||
func (f *fakeStore) record(op string) {
|
||||
if f.calls != nil {
|
||||
*f.calls = append(*f.calls, op+":"+f.name)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *fakeStore) Name() string { return f.name }
|
||||
|
||||
func (f *fakeStore) List(_ context.Context, _ string) ([]Entry, error) {
|
||||
f.record("list")
|
||||
if f.err != nil {
|
||||
return nil, f.err
|
||||
}
|
||||
return f.entries, nil
|
||||
}
|
||||
|
||||
func (f *fakeStore) Stat(_ context.Context, id string) (Entry, error) {
|
||||
f.record("stat")
|
||||
if f.err != nil {
|
||||
return Entry{}, f.err
|
||||
}
|
||||
return Entry{ID: id, Title: "t-" + f.name, Provider: f.name}, nil
|
||||
}
|
||||
|
||||
func (f *fakeStore) CreateFolder(_ context.Context, _, title string) (Entry, error) {
|
||||
f.record("mkdir")
|
||||
if f.err != nil {
|
||||
return Entry{}, f.err
|
||||
}
|
||||
return Entry{ID: "new", Title: title, Provider: f.name}, nil
|
||||
}
|
||||
|
||||
func (f *fakeStore) Upload(_ context.Context, _, title string, _ io.Reader) (Entry, error) {
|
||||
f.record("upload")
|
||||
return Entry{ID: "up", Title: title, Provider: f.name}, f.err
|
||||
}
|
||||
|
||||
func (f *fakeStore) Download(_ context.Context, _ string, _ io.Writer) (int64, error) {
|
||||
f.record("download")
|
||||
return 0, f.err
|
||||
}
|
||||
|
||||
func (f *fakeStore) Move(_ context.Context, _ []string, _ string) error {
|
||||
f.record("move")
|
||||
return f.err
|
||||
}
|
||||
|
||||
func (f *fakeStore) Copy(_ context.Context, _ []string, _ string) error {
|
||||
f.record("copy")
|
||||
return f.err
|
||||
}
|
||||
|
||||
func (f *fakeStore) Rename(_ context.Context, _, _ string) error {
|
||||
f.record("rename")
|
||||
return f.err
|
||||
}
|
||||
|
||||
func (f *fakeStore) Delete(_ context.Context, _ []string) error {
|
||||
f.record("delete")
|
||||
return f.err
|
||||
}
|
||||
|
||||
type fakeSearcher struct{ name string }
|
||||
|
||||
func (s *fakeSearcher) Name() string { return s.name }
|
||||
|
||||
func (s *fakeSearcher) Search(_ context.Context, _ SearchQuery) ([]SearchHit, error) {
|
||||
return []SearchHit{{Entry: Entry{Title: s.name}}}, nil
|
||||
}
|
||||
|
||||
func newFacadeTestClient(stores map[string]FileStore, read, write []string) *FileClient {
|
||||
return &FileClient{
|
||||
stores: stores,
|
||||
searchers: map[string]Searcher{},
|
||||
readOrder: read,
|
||||
writeOrder: write,
|
||||
}
|
||||
}
|
||||
|
||||
// TestFileClientIsFileStore guarantees the facade can stand in for the
|
||||
// interface anywhere a plain FileStore is expected.
|
||||
func TestFileClientIsFileStore(t *testing.T) {
|
||||
var _ FileStore = (*FileClient)(nil)
|
||||
}
|
||||
|
||||
func TestClientFilesPrefersRESTForReadsAndWrites(t *testing.T) {
|
||||
c := NewClient(Credentials{})
|
||||
f := c.Files()
|
||||
if got := f.Read().Name(); got != ProviderREST {
|
||||
t.Errorf("Read().Name() = %q, want %q", got, ProviderREST)
|
||||
}
|
||||
if got := f.Write().Name(); got != ProviderREST {
|
||||
t.Errorf("Write().Name() = %q, want %q", got, ProviderREST)
|
||||
}
|
||||
if got := f.Name(); got != ProviderREST {
|
||||
t.Errorf("Name() = %q, want %q", got, ProviderREST)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileClientPostgresTakesReadPriority(t *testing.T) {
|
||||
pg := &fakeStore{name: ProviderPG}
|
||||
f := newFacadeTestClient(
|
||||
map[string]FileStore{ProviderREST: &fakeStore{name: ProviderREST}, ProviderPG: pg},
|
||||
[]string{ProviderPG, ProviderREST},
|
||||
[]string{ProviderREST},
|
||||
)
|
||||
if got := f.Read().Name(); got != ProviderPG {
|
||||
t.Errorf("Read().Name() = %q, want %q", got, ProviderPG)
|
||||
}
|
||||
if got := f.Write().Name(); got != ProviderREST {
|
||||
t.Errorf("Write().Name() = %q, want %q (PG is read-only)", got, ProviderREST)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileClientRegisterStoreNormalizesName(t *testing.T) {
|
||||
pg := &fakeStore{name: "pg"}
|
||||
f := newFacadeTestClient(map[string]FileStore{}, []string{ProviderPG}, nil)
|
||||
f.RegisterStore(" POSTGRES ", pg)
|
||||
if got := f.Read(); got != pg {
|
||||
t.Fatalf("Read() = %v, want registered postgres store", got)
|
||||
}
|
||||
f.RegisterStore("", pg)
|
||||
f.RegisterStore("pg", nil)
|
||||
}
|
||||
|
||||
func TestFileClientReadFallsBackOnlyOnTransient(t *testing.T) {
|
||||
var calls []string
|
||||
primary := &fakeStore{name: "primary", err: fmt.Errorf("onlyoffice: list: 503 unavailable"), calls: &calls}
|
||||
secondary := &fakeStore{name: "secondary", entries: []Entry{{ID: "1"}}, calls: &calls}
|
||||
f := newFacadeTestClient(
|
||||
map[string]FileStore{"primary": primary, "secondary": secondary},
|
||||
[]string{"primary", "secondary"},
|
||||
nil,
|
||||
)
|
||||
got, err := f.List(context.Background(), "root")
|
||||
if err != nil {
|
||||
t.Fatalf("List: %v", err)
|
||||
}
|
||||
if len(got) != 1 || got[0].ID != "1" {
|
||||
t.Fatalf("List() = %+v, want secondary entry", got)
|
||||
}
|
||||
want := []string{"list:primary", "list:secondary"}
|
||||
if fmt.Sprint(calls) != fmt.Sprint(want) {
|
||||
t.Fatalf("call order = %v, want %v", calls, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileClientReadStopsOnPermanentError(t *testing.T) {
|
||||
var calls []string
|
||||
primary := &fakeStore{name: "primary", err: errors.New("onlyoffice: not found"), calls: &calls}
|
||||
secondary := &fakeStore{name: "secondary", entries: []Entry{{ID: "1"}}, calls: &calls}
|
||||
f := newFacadeTestClient(
|
||||
map[string]FileStore{"primary": primary, "secondary": secondary},
|
||||
[]string{"primary", "secondary"},
|
||||
nil,
|
||||
)
|
||||
if _, err := f.List(context.Background(), "root"); err == nil {
|
||||
t.Fatal("expected permanent error to be returned")
|
||||
}
|
||||
if len(calls) != 1 || calls[0] != "list:primary" {
|
||||
t.Fatalf("secondary backend must not run on a permanent error: %v", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileClientWriteUsesWriteBackend(t *testing.T) {
|
||||
var calls []string
|
||||
rest := &fakeStore{name: ProviderREST, calls: &calls}
|
||||
dav := &fakeStore{name: ProviderDAV, calls: &calls}
|
||||
f := newFacadeTestClient(
|
||||
map[string]FileStore{ProviderREST: rest, ProviderDAV: dav},
|
||||
[]string{ProviderREST},
|
||||
[]string{ProviderREST, ProviderDAV},
|
||||
)
|
||||
if _, err := f.CreateFolder(context.Background(), "p", "t"); err != nil {
|
||||
t.Fatalf("CreateFolder: %v", err)
|
||||
}
|
||||
if _, err := f.Upload(context.Background(), "p", "t", nil); err != nil {
|
||||
t.Fatalf("Upload: %v", err)
|
||||
}
|
||||
if len(calls) != 2 || calls[0] != "mkdir:rest" || calls[1] != "upload:rest" {
|
||||
t.Fatalf("write calls = %v, want REST", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileClientWriteWithoutBackend(t *testing.T) {
|
||||
f := newFacadeTestClient(map[string]FileStore{}, nil, nil)
|
||||
if err := f.Delete(context.Background(), []string{"1"}); !errors.Is(err, errNoWriteBackend) {
|
||||
t.Fatalf("Delete err = %v, want errNoWriteBackend", err)
|
||||
}
|
||||
if _, err := f.List(context.Background(), "root"); !errors.Is(err, errNoReadBackend) {
|
||||
t.Fatalf("List err = %v, want errNoReadBackend", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileClientSearchSelection(t *testing.T) {
|
||||
f := &FileClient{searchers: map[string]Searcher{}, searchOrder: []string{ProviderES}}
|
||||
_, err := f.Search()
|
||||
if err == nil || !strings.Contains(err.Error(), "ONLYOFFICE_ES_URL") {
|
||||
t.Fatalf("Search without backend = %v, want ONLYOFFICE_ES_URL hint", err)
|
||||
}
|
||||
es := &fakeSearcher{name: "fake-es"}
|
||||
f.RegisterSearcher(ProviderES, es)
|
||||
got, err := f.Search()
|
||||
if err != nil {
|
||||
t.Fatalf("Search: %v", err)
|
||||
}
|
||||
if got.Name() != "fake-es" {
|
||||
t.Fatalf("searcher = %q, want fake-es", got.Name())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user