Files
lgcosta e0bef77e31
ci / check (push) Successful in 4m12s
refactor: integra reconcile no boot, dedup categoria/pasta, CI, Go 1.24
- Boot chama ReconcileBlobs com log de órfãos (era código morto)
- treeNode/validTreeName/siblingDup/moveCreatesCycle compartilhados
  entre Create/Update Category/Folder (mesma regra, um só lugar)
- mapStoreErrorFor com mensagens 404 por recurso (nota/arquivo/pasta/categoria)
- revisionHash helper + teste GET /revisions/{hash} (200 + 404)
- ReconcileBlobs com lock curto (coleta sob RLock, I/O fora do lock)
- AddFile com LimitReader no stream (cap antes de gravar, não depois)
- DeleteFile/DeleteFolder transacionais com rollback em memória
- attachmentKind único no frontend (syntax/html/preview)
- CI: gofmt, vet, test -race, node --check; go.mod 1.24 = Dockerfile
- README: notas operacionais (memória, proxy, boot, SSE, 413/400)
2026-11-05 19:57:38 -03:00

167 lines
4.5 KiB
Go

package store
import (
"errors"
"os"
"strings"
"testing"
)
func mustFolder(t *testing.T, s *Store, name, parent string) *Folder {
t.Helper()
f, err := s.CreateFolder(name, parent)
if err != nil {
t.Fatalf("CreateFolder(%q): %v", name, err)
}
return f
}
func strPtr(s string) *string { return &s }
// writeBlobForTest grava um arquivo bruto no dir de blobs (simula órfão).
func writeBlobForTest(s *Store, path string) error {
return os.WriteFile(path, []byte("orfao"), 0o640)
}
func TestAddFileValidatesAndServesBlob(t *testing.T) {
s := newTestStore(t)
folder := mustFolder(t, s, "docs", "")
m, err := s.AddFile("rel.pdf", folder.ID, "", strings.NewReader("%PDF-1.4 fake"))
if err != nil {
t.Fatal(err)
}
if m.Size == 0 || m.MIME == "" || m.SHA256 == "" {
t.Fatalf("metadado incompleto: %+v", m)
}
if got := s.RootFiles(); len(got) != 0 {
t.Fatalf("arquivo com pasta não deveria estar na raiz: %d", len(got))
}
if _, err := s.AddFile("x.txt", "pasta-inexistente", "", strings.NewReader("x")); !errors.Is(err, ErrInvalid) {
t.Fatalf("pasta inexistente deveria dar ErrInvalid, deu %v", err)
}
if _, err := s.AddFile("y.txt", "", "nota-inexistente", strings.NewReader("y")); !errors.Is(err, ErrInvalid) {
t.Fatalf("nota inexistente deveria dar ErrInvalid, deu %v", err)
}
}
func TestAddFileRejectsPerFileCap(t *testing.T) {
s := newTestStore(t)
if _, err := s.AddFile("big.bin", "", "", infiniteReader{}); !errors.Is(err, ErrTooLarge) {
t.Fatalf("esperava ErrTooLarge, deu %v", err)
}
}
// infiniteReader gera bytes infinitos (para estourar o cap sem alocar).
type infiniteReader struct{}
func (infiniteReader) Read(p []byte) (int, error) {
for i := range p {
p[i] = 'A'
}
return len(p), nil
}
func TestAddFileConcurrencyHonorsQuota(t *testing.T) {
s := newTestStore(t)
payload := strings.Repeat("A", 1<<20) // 1 MiB
const n = 8
errs := make(chan error, n)
for range n {
go func() {
_, err := s.AddFile("f.bin", "", "", strings.NewReader(payload))
errs <- err
}()
}
for range n {
if err := <-errs; err != nil {
// Quota é 2 GiB: nenhum upload de 1 MiB deveria falhar aqui.
t.Fatalf("erro inesperado: %v", err)
}
}
removed, missing, err := s.ReconcileBlobs()
if err != nil {
t.Fatal(err)
}
if removed != 0 || len(missing) != 0 {
t.Fatalf("reconciliação após uploads ok: removed=%d missing=%v", removed, missing)
}
}
func TestDeleteFilePersistsBeforeUnlink(t *testing.T) {
s := newTestStore(t)
m, err := s.AddFile("a.txt", "", "", strings.NewReader("oi"))
if err != nil {
t.Fatal(err)
}
if err := s.DeleteFile(m.ID); err != nil {
t.Fatal(err)
}
if _, err := s.GetFile(m.ID); !errors.Is(err, ErrNotFound) {
t.Fatalf("esperava ErrNotFound após delete, deu %v", err)
}
if err := s.DeleteFile(m.ID); !errors.Is(err, ErrNotFound) {
t.Fatalf("delete duplo deveria dar ErrNotFound, deu %v", err)
}
}
func TestDeleteFolderPersistsBeforeUnlink(t *testing.T) {
s := newTestStore(t)
f := mustFolder(t, s, "p", "")
if _, err := s.AddFile("a.txt", f.ID, "", strings.NewReader("conteudo")); err != nil {
t.Fatal(err)
}
if err := s.DeleteFolder(f.ID); err != nil {
t.Fatal(err)
}
if len(s.fileMetas) != 0 {
t.Fatalf("arquivos deveriam ter sumido: %+v", s.fileMetas)
}
removed, missing, err := s.ReconcileBlobs()
if err != nil {
t.Fatal(err)
}
if removed != 0 || len(missing) != 0 {
t.Fatalf("órfãos após delete limpo: removed=%d missing=%v", removed, missing)
}
}
func TestReconcileBlobsRemovesOrphans(t *testing.T) {
s := newTestStore(t)
if _, err := s.AddFile("a.txt", "", "", strings.NewReader("conteudo")); err != nil {
t.Fatal(err)
}
// Simula blob órfão de falha de commit: arquivo no disco sem metadado.
if err := writeBlobForTest(s, s.fileBlobPath("sem-metadado", ".bin")); err != nil {
t.Fatal(err)
}
removed, missing, err := s.ReconcileBlobs()
if err != nil {
t.Fatal(err)
}
if removed != 1 {
t.Fatalf("esperava 1 órfão removido, removeu %d", removed)
}
if len(missing) != 0 {
t.Fatalf("nenhum metadado deveria estar sem blob: %v", missing)
}
}
func TestMoveFileValidation(t *testing.T) {
s := newTestStore(t)
m, err := s.AddFile("a.txt", "", "", strings.NewReader("x"))
if err != nil {
t.Fatal(err)
}
if _, err := s.MoveFile(m.ID, "", strPtr("pasta-inexistente"), nil); !errors.Is(err, ErrInvalid) {
t.Fatalf("move para pasta inexistente deveria dar ErrInvalid, deu %v", err)
}
f := mustFolder(t, s, "ok", "")
got, err := s.MoveFile(m.ID, "", &f.ID, nil)
if err != nil {
t.Fatal(err)
}
if got.Folder != f.ID {
t.Fatalf("esperava pasta %s, veio %s", f.ID, got.Folder)
}
}