Files
synapbus/internal/attachments/service_test.go
T
Algis DumbrisandClaude Opus 4.6 0e28c0b45e feat: fix attachment handling — display in DMs, enrich in MCP, allow all file types
- Show attachment previews on DM messages (was missing, only channels had it)
- Add file upload button to DM compose bar with paperclip icon
- Enrich messages with attachment data in all MCP bridge functions
  (read_inbox, claim_messages, search, channel_messages, list_by_state)
- Remove file type restrictions — allow any file type, keep 50MB size limit
- Rebuild web dist

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 13:00:28 +02:00

340 lines
7.6 KiB
Go

package attachments
import (
"bytes"
"context"
"database/sql"
"io"
"log/slog"
"path/filepath"
"strings"
"testing"
)
func newTestService(t *testing.T) (*Service, *sql.DB) {
t.Helper()
db := newTestDB(t)
dir := t.TempDir()
cas, err := NewCAS(filepath.Join(dir, "attachments"), slog.Default())
if err != nil {
t.Fatalf("NewCAS: %v", err)
}
store := NewSQLiteStore(db, slog.Default())
svc := NewService(store, cas, slog.Default())
return svc, db
}
func TestService_Upload(t *testing.T) {
svc, _ := newTestService(t)
ctx := context.Background()
content := []byte("service upload test content")
wantHash := sha256Hex(content)
result, err := svc.Upload(ctx, UploadRequest{
Content: bytes.NewReader(content),
Filename: "test.txt",
UploadedBy: "agent-a",
})
if err != nil {
t.Fatalf("Upload: %v", err)
}
if result.Hash != wantHash {
t.Errorf("hash = %s, want %s", result.Hash, wantHash)
}
if result.Size != int64(len(content)) {
t.Errorf("size = %d, want %d", result.Size, len(content))
}
if result.Filename != "test.txt" {
t.Errorf("filename = %s, want test.txt", result.Filename)
}
}
func TestService_Upload_MIMEDetection(t *testing.T) {
svc, _ := newTestService(t)
ctx := context.Background()
// PNG magic bytes.
pngContent := []byte{0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00}
result, err := svc.Upload(ctx, UploadRequest{
Content: bytes.NewReader(pngContent),
Filename: "image.png",
UploadedBy: "agent-a",
})
if err != nil {
t.Fatalf("Upload: %v", err)
}
if result.MIMEType != "image/png" {
t.Errorf("mime_type = %s, want image/png", result.MIMEType)
}
}
func TestService_Upload_DefaultFilename(t *testing.T) {
svc, _ := newTestService(t)
ctx := context.Background()
content := []byte("no filename content here")
result, err := svc.Upload(ctx, UploadRequest{
Content: bytes.NewReader(content),
UploadedBy: "agent-a",
})
if err != nil {
t.Fatalf("Upload: %v", err)
}
if result.Filename == "" {
t.Error("expected a default filename, got empty")
}
}
func TestService_Upload_EmptyFile(t *testing.T) {
svc, _ := newTestService(t)
ctx := context.Background()
_, err := svc.Upload(ctx, UploadRequest{
Content: bytes.NewReader([]byte{}),
Filename: "empty.txt",
UploadedBy: "agent-a",
})
if err != ErrEmptyFile {
t.Errorf("expected ErrEmptyFile, got %v", err)
}
}
func TestService_Upload_SizeLimit(t *testing.T) {
svc, _ := newTestService(t)
ctx := context.Background()
// Create a reader that yields MaxFileSize+1 bytes.
oversized := make([]byte, MaxFileSize+1)
for i := range oversized {
oversized[i] = 'x'
}
_, err := svc.Upload(ctx, UploadRequest{
Content: bytes.NewReader(oversized),
Filename: "toobig.bin",
UploadedBy: "agent-a",
})
if err != ErrFileTooLarge {
t.Errorf("expected ErrFileTooLarge, got %v", err)
}
}
func TestService_Download(t *testing.T) {
svc, _ := newTestService(t)
ctx := context.Background()
content := []byte("download test content")
uploadResult, err := svc.Upload(ctx, UploadRequest{
Content: bytes.NewReader(content),
Filename: "download.txt",
UploadedBy: "agent-a",
})
if err != nil {
t.Fatalf("Upload: %v", err)
}
t.Run("successful download", func(t *testing.T) {
result, err := svc.Download(ctx, uploadResult.Hash)
if err != nil {
t.Fatalf("Download: %v", err)
}
defer result.Content.Close()
got, err := io.ReadAll(result.Content)
if err != nil {
t.Fatalf("ReadAll: %v", err)
}
if !bytes.Equal(got, content) {
t.Error("downloaded content does not match uploaded content")
}
if result.Filename != "download.txt" {
t.Errorf("filename = %s, want download.txt", result.Filename)
}
})
t.Run("not found", func(t *testing.T) {
_, err := svc.Download(ctx, "deadbeef"+strings.Repeat("0", 56))
if err != ErrNotFound {
t.Errorf("expected ErrNotFound, got %v", err)
}
})
}
func TestService_Dedup(t *testing.T) {
svc, _ := newTestService(t)
ctx := context.Background()
content := []byte("dedup test content")
wantHash := sha256Hex(content)
r1, err := svc.Upload(ctx, UploadRequest{
Content: bytes.NewReader(content),
Filename: "version1.txt",
UploadedBy: "agent-a",
})
if err != nil {
t.Fatalf("Upload 1: %v", err)
}
r2, err := svc.Upload(ctx, UploadRequest{
Content: bytes.NewReader(content),
Filename: "version2.txt",
UploadedBy: "agent-b",
})
if err != nil {
t.Fatalf("Upload 2: %v", err)
}
if r1.Hash != r2.Hash {
t.Errorf("hashes differ: %s vs %s", r1.Hash, r2.Hash)
}
if r1.Hash != wantHash {
t.Errorf("hash = %s, want %s", r1.Hash, wantHash)
}
}
func TestService_Upload_FileTypeValidation(t *testing.T) {
// PNG magic bytes.
pngContent := []byte{0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00}
// PDF magic bytes.
pdfContent := []byte("%PDF-1.4 some pdf content here")
// Plain text content.
textContent := []byte("just some plain text content")
tests := []struct {
name string
content []byte
filename string
mimeType string
wantErr error
}{
{
name: "valid image upload",
content: pngContent,
filename: "photo.png",
wantErr: nil,
},
{
name: "valid PDF upload",
content: pdfContent,
filename: "report.pdf",
wantErr: nil,
},
{
name: "valid text file upload",
content: textContent,
filename: "notes.txt",
wantErr: nil,
},
{
name: "zip upload allowed",
content: []byte("not real zip content"),
filename: "archive.zip",
mimeType: "application/zip",
wantErr: nil,
},
{
name: "executable upload allowed",
content: []byte{0x7f, 0x45, 0x4c, 0x46},
filename: "program.exe",
mimeType: "application/x-executable",
wantErr: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
svc, _ := newTestService(t)
ctx := context.Background()
_, err := svc.Upload(ctx, UploadRequest{
Content: bytes.NewReader(tt.content),
Filename: tt.filename,
MIMEType: tt.mimeType,
UploadedBy: "agent-a",
})
if tt.wantErr != nil {
if err != tt.wantErr {
t.Errorf("expected error %v, got %v", tt.wantErr, err)
}
return
}
if err != nil {
t.Fatalf("Upload: %v", err)
}
})
}
}
func TestService_GarbageCollect(t *testing.T) {
svc, db := newTestService(t)
ctx := context.Background()
// Upload an orphan (no message_id).
orphanContent := []byte("orphan gc content")
_, err := svc.Upload(ctx, UploadRequest{
Content: bytes.NewReader(orphanContent),
Filename: "orphan.txt",
UploadedBy: "agent-a",
})
if err != nil {
t.Fatalf("Upload orphan: %v", err)
}
// Upload a linked attachment.
msgID := seedMessage(t, db)
linkedContent := []byte("linked gc content")
linkedResult, err := svc.Upload(ctx, UploadRequest{
Content: bytes.NewReader(linkedContent),
Filename: "linked.txt",
MessageID: &msgID,
UploadedBy: "agent-a",
})
if err != nil {
t.Fatalf("Upload linked: %v", err)
}
// Run GC.
gc, err := svc.GarbageCollect(ctx)
if err != nil {
t.Fatalf("GarbageCollect: %v", err)
}
if gc.FilesRemoved != 1 {
t.Errorf("files_removed = %d, want 1", gc.FilesRemoved)
}
// Verify linked file still downloadable.
result, err := svc.Download(ctx, linkedResult.Hash)
if err != nil {
t.Fatalf("Download linked after GC: %v", err)
}
result.Content.Close()
}
func TestService_GarbageCollect_Empty(t *testing.T) {
svc, _ := newTestService(t)
ctx := context.Background()
gc, err := svc.GarbageCollect(ctx)
if err != nil {
t.Fatalf("GarbageCollect: %v", err)
}
if gc.FilesRemoved != 0 {
t.Errorf("files_removed = %d, want 0", gc.FilesRemoved)
}
if gc.BytesReclaimed != 0 {
t.Errorf("bytes_reclaimed = %d, want 0", gc.BytesReclaimed)
}
}