feat(files): canonical Entry + FileStore REST/DAV adapters (#35)
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 18s
Tests / Test (Go stable) (pull_request) Successful in 18s
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 18s
Tests / Test (Go stable) (pull_request) Successful in 18s
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
//go:build integration
|
||||
|
||||
package onlyoffice
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestIntegrationFileStores runs the same operation set (create folder, upload,
|
||||
// list, stat, download, move, copy, rename, delete) through the REST and DAV
|
||||
// FileStore adapters against a throwaway project Documents folder. Destructive
|
||||
// — only run against instances you own.
|
||||
//
|
||||
// The Documents fileops API is asynchronous: a move/copy/delete is accepted
|
||||
// immediately and becomes visible a moment later, so effects are polled.
|
||||
func TestIntegrationFileStores(t *testing.T) {
|
||||
c := liveClient(t)
|
||||
t.Cleanup(func() { cleanupTestProjects(t, c) })
|
||||
ctx := context.Background()
|
||||
|
||||
suffix := time.Now().UTC().Format("20060102-150405")
|
||||
project, err := c.CreateProject(NewProjectRequest{
|
||||
Title: testProjectPrefix + "store-" + suffix,
|
||||
Description: "go-onlyoffice file store integration",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateProject: %v", err)
|
||||
}
|
||||
if project.ID == nil {
|
||||
t.Fatal("created project without id")
|
||||
}
|
||||
root, err := c.projectFolderID(ctx, strconv.Itoa(*project.ID))
|
||||
if err != nil {
|
||||
t.Fatalf("projectFolderID: %v", err)
|
||||
}
|
||||
|
||||
for _, backend := range []string{ProviderREST, ProviderDAV} {
|
||||
t.Run(backend, func(t *testing.T) {
|
||||
testFileStoreOps(t, ctx, c, c.FileStore(backend), root, suffix)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testFileStoreOps(t *testing.T, ctx context.Context, c *Client, store FileStore, root, suffix string) {
|
||||
t.Helper()
|
||||
content := []byte("file store " + store.Name() + " " + suffix + "\n")
|
||||
|
||||
src, err := store.CreateFolder(ctx, root, "fs-src-"+suffix)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateFolder src: %v", err)
|
||||
}
|
||||
if src.Kind != Folder || src.ID == "" {
|
||||
t.Fatalf("created src folder: %+v", src)
|
||||
}
|
||||
dst, err := store.CreateFolder(ctx, root, "fs-dst-"+suffix)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateFolder dst: %v", err)
|
||||
}
|
||||
if dst.Kind != Folder || dst.ID == "" {
|
||||
t.Fatalf("created dst folder: %+v", dst)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := c.DeleteDavItems(ctx, []string{src.ID, dst.ID}, nil); err != nil {
|
||||
t.Logf("cleanup folders: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
up, err := store.Upload(ctx, src.ID, "doc-"+suffix+".txt", bytes.NewReader(content))
|
||||
if err != nil {
|
||||
t.Fatalf("Upload: %v", err)
|
||||
}
|
||||
if up.Kind != File || up.ID == "" {
|
||||
t.Fatalf("uploaded entry: %+v", up)
|
||||
}
|
||||
if !waitEntry(ctx, store, src.ID, up.ID, 15*time.Second) {
|
||||
t.Fatalf("uploaded %s not listed in src", up.ID)
|
||||
}
|
||||
|
||||
st, err := store.Stat(ctx, up.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Stat: %v", err)
|
||||
}
|
||||
if st.ID != up.ID || st.Kind != File {
|
||||
t.Fatalf("stat = %+v", st)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
n, err := store.Download(ctx, up.ID, &buf)
|
||||
if err != nil {
|
||||
t.Fatalf("Download: %v", err)
|
||||
}
|
||||
if n != int64(len(content)) || !bytes.Equal(buf.Bytes(), content) {
|
||||
t.Fatalf("download mismatch: got %d bytes %q want %d", n, buf.String(), len(content))
|
||||
}
|
||||
|
||||
moveEventually(t, ctx, store, up.ID, dst.ID)
|
||||
if !waitEntry(ctx, store, dst.ID, up.ID, 20*time.Second) {
|
||||
t.Fatalf("moved file %s not in dst", up.ID)
|
||||
}
|
||||
|
||||
if err := store.Copy(ctx, []string{up.ID}, src.ID); err != nil {
|
||||
t.Fatalf("Copy: %v", err)
|
||||
}
|
||||
copied := waitOtherFile(ctx, store, src.ID, up.ID, 20*time.Second)
|
||||
if copied == nil {
|
||||
t.Fatalf("no copy found in src after Copy")
|
||||
}
|
||||
|
||||
newTitle := "renamed-" + suffix + ".txt"
|
||||
renameEventually(t, ctx, store, up.ID, newTitle)
|
||||
|
||||
if err := store.Delete(ctx, []string{up.ID, copied.ID}); err != nil {
|
||||
t.Fatalf("Delete: %v", err)
|
||||
}
|
||||
if !waitNoEntry(ctx, store, dst.ID, up.ID, 20*time.Second) {
|
||||
t.Fatalf("file %s still present in dst after delete", up.ID)
|
||||
}
|
||||
if !waitNoEntry(ctx, store, src.ID, copied.ID, 20*time.Second) {
|
||||
t.Fatalf("copy %s still present in src after delete", copied.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// moveEventually issues Move and retries while the operation is not visible yet
|
||||
// (the fileops API accepts asynchronously and occasionally rejects a move that
|
||||
// raced the just-finished upload).
|
||||
func moveEventually(t *testing.T, ctx context.Context, store FileStore, id, dstID string) {
|
||||
t.Helper()
|
||||
var lastErr error
|
||||
for attempt := 0; attempt < 5; attempt++ {
|
||||
if lastErr = store.Move(ctx, []string{id}, dstID); lastErr == nil {
|
||||
if waitEntry(ctx, store, dstID, id, 6*time.Second) {
|
||||
return
|
||||
}
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
t.Fatalf("Move %s -> %s: %v", id, dstID, lastErr)
|
||||
}
|
||||
|
||||
func renameEventually(t *testing.T, ctx context.Context, store FileStore, id, title string) {
|
||||
t.Helper()
|
||||
var lastErr error
|
||||
for attempt := 0; attempt < 5; attempt++ {
|
||||
if lastErr = store.Rename(ctx, id, title); lastErr == nil {
|
||||
if e, err := store.Stat(ctx, id); err == nil && e.Title == title {
|
||||
return
|
||||
}
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
t.Fatalf("Rename %s -> %q: %v", id, title, lastErr)
|
||||
}
|
||||
|
||||
func waitEntry(ctx context.Context, store FileStore, parentID, id string, d time.Duration) bool {
|
||||
deadline := time.Now().Add(d)
|
||||
for time.Now().Before(deadline) {
|
||||
if list, err := store.List(ctx, parentID); err == nil && entryByID(list, id) != nil {
|
||||
return true
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func waitNoEntry(ctx context.Context, store FileStore, parentID, id string, d time.Duration) bool {
|
||||
deadline := time.Now().Add(d)
|
||||
for time.Now().Before(deadline) {
|
||||
if list, err := store.List(ctx, parentID); err == nil && entryByID(list, id) == nil {
|
||||
return true
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func waitOtherFile(ctx context.Context, store FileStore, parentID, id string, d time.Duration) *Entry {
|
||||
deadline := time.Now().Add(d)
|
||||
for time.Now().Before(deadline) {
|
||||
if list, err := store.List(ctx, parentID); err == nil {
|
||||
if e := firstFileOtherThan(list, id); e != nil {
|
||||
return e
|
||||
}
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func entryByID(entries []Entry, id string) *Entry {
|
||||
for i := range entries {
|
||||
if entries[i].ID == id {
|
||||
return &entries[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func firstFileOtherThan(entries []Entry, id string) *Entry {
|
||||
for i := range entries {
|
||||
if entries[i].Kind == File && entries[i].ID != id {
|
||||
return &entries[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user