feat: store layer with indexes, optimistic concurrency, GridFS, credential crypto (phase 2)
- mongo-driver v2 connection with bounded startup retry + /readyz gate - idempotent creation of all §4.9 indexes - UpdateVersioned: version-filtered updates, conflict vs not-found errors - AES-256-GCM Seal/Open for ticketing credentials, base64(nonce|ct) - GridFS file store: MIME sniffing, MAX_UPLOAD_MB cap, sha256 metadata - HMAC-signed short-lived file URL tokens (§5.1, 1h TTL) - integration tests against compose Mongo via docker-compose.test.yml overlay Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
//go:build integration
|
||||
|
||||
package files
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"bountyboard/internal/testutil"
|
||||
)
|
||||
|
||||
func TestSaveOpenRoundTrip(t *testing.T) {
|
||||
db := testutil.MongoDB(t)
|
||||
s := NewStore(db, 25)
|
||||
ctx := context.Background()
|
||||
|
||||
content := []byte("%PDF-1.4 fake pdf content for sniffing")
|
||||
meta, err := s.Save(ctx, ScopeTask, "01JOWNER", "spec.pdf", "", bytes.NewReader(content))
|
||||
if err != nil {
|
||||
t.Fatalf("Save: %v", err)
|
||||
}
|
||||
if meta.MimeType != "application/pdf" {
|
||||
t.Errorf("sniffed mime = %q, want application/pdf", meta.MimeType)
|
||||
}
|
||||
if meta.Size != int64(len(content)) {
|
||||
t.Errorf("size = %d, want %d", meta.Size, len(content))
|
||||
}
|
||||
wantSum := sha256.Sum256(content)
|
||||
if meta.SHA256 != hex.EncodeToString(wantSum[:]) {
|
||||
t.Errorf("sha256 mismatch")
|
||||
}
|
||||
|
||||
rc, got, err := s.Open(ctx, meta.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Open: %v", err)
|
||||
}
|
||||
defer rc.Close()
|
||||
data, err := io.ReadAll(rc)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(data, content) {
|
||||
t.Error("downloaded content differs")
|
||||
}
|
||||
if got.OwnerID != "01JOWNER" || got.Scope != ScopeTask || got.Name != "spec.pdf" {
|
||||
t.Errorf("metadata round trip: %+v", got)
|
||||
}
|
||||
if got.SHA256 != meta.SHA256 || got.MimeType != meta.MimeType {
|
||||
t.Errorf("stored metadata mismatch: %+v vs %+v", got, meta)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeclaredMimeUsedWhenSniffInconclusive(t *testing.T) {
|
||||
db := testutil.MongoDB(t)
|
||||
s := NewStore(db, 25)
|
||||
|
||||
meta, err := s.Save(context.Background(), ScopeChat, "01JOWNER", "data.bin",
|
||||
"application/x-custom", bytes.NewReader([]byte{0x01, 0x02, 0x03}))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if meta.MimeType != "application/x-custom" {
|
||||
t.Errorf("mime = %q, want declared application/x-custom", meta.MimeType)
|
||||
}
|
||||
|
||||
// But a confident sniff overrides a lying declaration.
|
||||
meta2, err := s.Save(context.Background(), ScopeChat, "01JOWNER", "fake.txt",
|
||||
"text/plain", strings.NewReader("\x89PNG\r\n\x1a\n123456789"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if meta2.MimeType != "image/png" {
|
||||
t.Errorf("mime = %q, want sniffed image/png", meta2.MimeType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSizeLimitEnforced(t *testing.T) {
|
||||
db := testutil.MongoDB(t)
|
||||
s := NewStore(db, 1) // 1 MiB cap
|
||||
|
||||
big := io.LimitReader(neverEnding('x'), 1<<20+1)
|
||||
_, err := s.Save(context.Background(), ScopeChat, "01JOWNER", "big.bin", "", big)
|
||||
if !errors.Is(err, ErrTooLarge) {
|
||||
t.Fatalf("want ErrTooLarge, got %v", err)
|
||||
}
|
||||
|
||||
// No orphaned file documents may remain.
|
||||
n, err := s.bucket.GetFilesCollection().CountDocuments(context.Background(), struct{}{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n != 0 {
|
||||
t.Errorf("oversize upload left %d file docs behind", n)
|
||||
}
|
||||
|
||||
// Exactly at the limit is fine.
|
||||
ok := io.LimitReader(neverEnding('x'), 1<<20)
|
||||
if _, err := s.Save(context.Background(), ScopeChat, "01JOWNER", "exact.bin", "", ok); err != nil {
|
||||
t.Fatalf("upload at exactly the limit: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenMissingFile(t *testing.T) {
|
||||
db := testutil.MongoDB(t)
|
||||
s := NewStore(db, 25)
|
||||
if _, _, err := s.Open(context.Background(), "01JNOPE"); !errors.Is(err, ErrNotFound) {
|
||||
t.Fatalf("want ErrNotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelete(t *testing.T) {
|
||||
db := testutil.MongoDB(t)
|
||||
s := NewStore(db, 25)
|
||||
ctx := context.Background()
|
||||
|
||||
meta, err := s.Save(ctx, ScopeAvatar, "01JOWNER", "a.txt", "", strings.NewReader("hello"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.Delete(ctx, meta.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, _, err := s.Open(ctx, meta.ID); !errors.Is(err, ErrNotFound) {
|
||||
t.Fatalf("want ErrNotFound after delete, got %v", err)
|
||||
}
|
||||
if err := s.Delete(ctx, meta.ID); !errors.Is(err, ErrNotFound) {
|
||||
t.Fatalf("double delete: want ErrNotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// neverEnding is an infinite reader of one repeated byte.
|
||||
type neverEnding byte
|
||||
|
||||
func (b neverEnding) Read(p []byte) (int, error) {
|
||||
for i := range p {
|
||||
p[i] = byte(b)
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
Reference in New Issue
Block a user