//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 }