feat(files): read-only SQL file store over Community Server DB (#36)
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 1m56s
Tests / Test (Go stable) (pull_request) Successful in 1m57s
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 1m56s
Tests / Test (Go stable) (pull_request) Successful in 1m57s
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
//go:build integration
|
||||
|
||||
package onlyoffice
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestIntegrationPGStore exercises the read-only SQL backend against the live
|
||||
// Community Server database and cross-checks list/stat/download with the REST
|
||||
// FileStore. It needs ONLYOFFICE_DSN plus the usual ONLYOFFICE_URL/USER/PASS;
|
||||
// ONLYOFFICE_PG_TEST_FILE_ID / ONLYOFFICE_PG_TEST_FOLDER_ID pick a real file
|
||||
// (a file reachable over REST too). Download streams from MinIO, so it also
|
||||
// needs MINIO_ACCESS_KEY/MINIO_SECRET_KEY.
|
||||
//
|
||||
// The live Community Server runs on MySQL; PostgreSQL is supported by the same
|
||||
// code path when the DSN says so.
|
||||
func TestIntegrationPGStore(t *testing.T) {
|
||||
cfg := PGConfigFromEnv()
|
||||
if strings.TrimSpace(cfg.DSN) == "" {
|
||||
t.Skip("ONLYOFFICE_DSN not set — skipping SQL store integration test")
|
||||
}
|
||||
store, err := NewPGStore(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("NewPGStore: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = store.Close() })
|
||||
t.Logf("sql store backend: %s", store.Name())
|
||||
ctx := context.Background()
|
||||
|
||||
if err := testPGStoreReadOnly(ctx, store); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fileID := strings.TrimSpace(os.Getenv("ONLYOFFICE_PG_TEST_FILE_ID"))
|
||||
folderID := strings.TrimSpace(os.Getenv("ONLYOFFICE_PG_TEST_FOLDER_ID"))
|
||||
if fileID == "" || folderID == "" {
|
||||
t.Skip("ONLYOFFICE_PG_TEST_FILE_ID / ONLYOFFICE_PG_TEST_FOLDER_ID not set — skipping live comparison")
|
||||
}
|
||||
|
||||
c := liveClient(t)
|
||||
rest := c.Files()
|
||||
|
||||
dbFile, err := store.Stat(ctx, fileID)
|
||||
if err != nil {
|
||||
t.Fatalf("sql Stat(%s): %v", fileID, err)
|
||||
}
|
||||
restFile, err := rest.Stat(ctx, fileID)
|
||||
if err != nil {
|
||||
t.Fatalf("rest Stat(%s): %v", fileID, err)
|
||||
}
|
||||
if dbFile.Kind != File {
|
||||
t.Errorf("sql kind = %v, want file", dbFile.Kind)
|
||||
}
|
||||
if dbFile.ID != restFile.ID || dbFile.Title != restFile.Title || dbFile.ParentID != restFile.ParentID {
|
||||
t.Errorf("stat mismatch sql=%+v rest=%+v", dbFile, restFile)
|
||||
}
|
||||
// GetFile omits contentLength, so size is only comparable when REST has it.
|
||||
if restFile.Size > 0 && dbFile.Size != restFile.Size {
|
||||
t.Errorf("size sql=%d rest=%d", dbFile.Size, restFile.Size)
|
||||
}
|
||||
if d := dbFile.Modified.Sub(restFile.Modified); d > 2*time.Minute || d < -2*time.Minute {
|
||||
t.Errorf("modified sql=%v rest=%v", dbFile.Modified, restFile.Modified)
|
||||
}
|
||||
|
||||
list, err := store.List(ctx, folderID)
|
||||
if err != nil {
|
||||
t.Fatalf("sql List(%s): %v", folderID, err)
|
||||
}
|
||||
if entryByID(list, fileID) == nil {
|
||||
t.Errorf("file %s not in sql List(%s)", fileID, folderID)
|
||||
}
|
||||
|
||||
// Every file the REST layer can see in the folder must be in the SQL list
|
||||
// (the SQL store sees more, so only assert this direction).
|
||||
restList, err := rest.List(ctx, folderID)
|
||||
if err != nil {
|
||||
t.Fatalf("rest List(%s): %v", folderID, err)
|
||||
}
|
||||
dbIDs := make(map[string]bool, len(list))
|
||||
for _, e := range list {
|
||||
dbIDs[e.ID] = true
|
||||
}
|
||||
for _, e := range restList {
|
||||
if e.Kind == File && !dbIDs[e.ID] {
|
||||
t.Errorf("rest file %s (%q) missing from sql list", e.ID, e.Title)
|
||||
}
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
n, err := store.Download(ctx, fileID, &buf)
|
||||
if err != nil {
|
||||
t.Fatalf("sql Download(%s): %v", fileID, err)
|
||||
}
|
||||
if n == 0 || n != dbFile.Size {
|
||||
t.Errorf("sql Download = %d bytes, stat says %d", n, dbFile.Size)
|
||||
}
|
||||
if os.Getenv("MINIO_ACCESS_KEY") != "" && os.Getenv("MINIO_SECRET_KEY") != "" {
|
||||
var restBuf bytes.Buffer
|
||||
rn, err := rest.Download(ctx, fileID, &restBuf)
|
||||
if err != nil {
|
||||
t.Fatalf("rest Download(%s): %v", fileID, err)
|
||||
}
|
||||
if rn != n || !bytes.Equal(restBuf.Bytes(), buf.Bytes()) {
|
||||
t.Errorf("download mismatch sql=%d rest=%d bytes", n, rn)
|
||||
}
|
||||
} else {
|
||||
t.Log("MINIO_ACCESS_KEY/MINIO_SECRET_KEY not set — REST download cross-check skipped")
|
||||
}
|
||||
}
|
||||
|
||||
// testPGStoreReadOnly asserts that every write method returns ErrReadOnly.
|
||||
func testPGStoreReadOnly(ctx context.Context, s *pgStore) error {
|
||||
if _, err := s.CreateFolder(ctx, "1", "x"); !errors.Is(err, ErrReadOnly) {
|
||||
return errors.New("CreateFolder did not return ErrReadOnly")
|
||||
}
|
||||
if _, err := s.Upload(ctx, "1", "x", strings.NewReader("x")); !errors.Is(err, ErrReadOnly) {
|
||||
return errors.New("Upload did not return ErrReadOnly")
|
||||
}
|
||||
if err := s.Move(ctx, []string{"1"}, "2"); !errors.Is(err, ErrReadOnly) {
|
||||
return errors.New("Move did not return ErrReadOnly")
|
||||
}
|
||||
if err := s.Copy(ctx, []string{"1"}, "2"); !errors.Is(err, ErrReadOnly) {
|
||||
return errors.New("Copy did not return ErrReadOnly")
|
||||
}
|
||||
if err := s.Rename(ctx, "1", "x"); !errors.Is(err, ErrReadOnly) {
|
||||
return errors.New("Rename did not return ErrReadOnly")
|
||||
}
|
||||
if err := s.Delete(ctx, []string{"1"}); !errors.Is(err, ErrReadOnly) {
|
||||
return errors.New("Delete did not return ErrReadOnly")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user