- Updated `logrus` dependency from v1.9.3 to v1.9.4 and `golang.org/x/sys` from v0.38.0 to v0.40.0 for improved functionality and security. - Introduced a `Fork` method in the agent context to create child contexts for concurrent agent and LLM calls, preventing race conditions on shared state during batch operations. - Enhanced the `Orchestrator` methods to utilize forked contexts, ensuring independent execution of agent calls without interference.
1132 lines
31 KiB
Go
1132 lines
31 KiB
Go
package attachment
|
||
|
||
import (
|
||
"encoding/base64"
|
||
"fmt"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/yaoapp/gou/process"
|
||
"github.com/yaoapp/yao/config"
|
||
"github.com/yaoapp/yao/test"
|
||
)
|
||
|
||
func TestProcessSave(t *testing.T) {
|
||
test.Prepare(t, config.Conf)
|
||
defer test.Clean()
|
||
|
||
// Register default uploader for testing
|
||
manager, err := RegisterDefault("data.local")
|
||
if err != nil {
|
||
t.Fatalf("Failed to register manager: %v", err)
|
||
}
|
||
_ = manager
|
||
|
||
// Test 1: Save with data URI format
|
||
t.Run("SaveWithDataURI", func(t *testing.T) {
|
||
content := "Hello, World!"
|
||
base64Content := base64.StdEncoding.EncodeToString([]byte(content))
|
||
dataURI := fmt.Sprintf("data:text/plain;base64,%s", base64Content)
|
||
|
||
p := process.New("attachment.Save", "data.local", dataURI, "hello.txt")
|
||
result := processSave(p)
|
||
|
||
if err, ok := result.(error); ok {
|
||
t.Fatalf("Failed to save file: %v", err)
|
||
}
|
||
|
||
file, ok := result.(*File)
|
||
if !ok {
|
||
t.Fatalf("Expected *File, got %T", result)
|
||
}
|
||
|
||
if file.ID == "" {
|
||
t.Error("File ID should not be empty")
|
||
}
|
||
|
||
if file.Filename != "hello.txt" {
|
||
t.Errorf("Expected filename 'hello.txt', got '%s'", file.Filename)
|
||
}
|
||
|
||
if !strings.HasPrefix(file.ContentType, "text/plain") {
|
||
t.Errorf("Expected content type 'text/plain', got '%s'", file.ContentType)
|
||
}
|
||
|
||
t.Logf("Saved file - ID: %s, Filename: %s, ContentType: %s", file.ID, file.Filename, file.ContentType)
|
||
})
|
||
|
||
// Test 2: Save with plain base64 (no data URI header) - use text/plain to pass allowed types
|
||
t.Run("SaveWithPlainBase64", func(t *testing.T) {
|
||
content := "Plain base64 content"
|
||
base64Content := base64.StdEncoding.EncodeToString([]byte(content))
|
||
// Without data URI header, we need to provide a filename with allowed extension
|
||
// or use data URI format. Let's test with text file extension.
|
||
dataURI := fmt.Sprintf("data:text/plain;base64,%s", base64Content)
|
||
|
||
p := process.New("attachment.Save", "data.local", dataURI, "plain.txt")
|
||
result := processSave(p)
|
||
|
||
if err, ok := result.(error); ok {
|
||
t.Fatalf("Failed to save file: %v", err)
|
||
}
|
||
|
||
file, ok := result.(*File)
|
||
if !ok {
|
||
t.Fatalf("Expected *File, got %T", result)
|
||
}
|
||
|
||
if file.ID == "" {
|
||
t.Error("File ID should not be empty")
|
||
}
|
||
|
||
// With data URI, content type should be text/plain
|
||
if !strings.HasPrefix(file.ContentType, "text/plain") {
|
||
t.Errorf("Expected content type 'text/plain', got '%s'", file.ContentType)
|
||
}
|
||
})
|
||
|
||
// Test 3: Save image with data URI
|
||
t.Run("SaveImageDataURI", func(t *testing.T) {
|
||
// Minimal valid PNG (1x1 pixel transparent PNG)
|
||
pngBase64 := "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
|
||
dataURI := fmt.Sprintf("data:image/png;base64,%s", pngBase64)
|
||
|
||
p := process.New("attachment.Save", "data.local", dataURI, "pixel.png")
|
||
result := processSave(p)
|
||
|
||
if err, ok := result.(error); ok {
|
||
t.Fatalf("Failed to save image: %v", err)
|
||
}
|
||
|
||
file, ok := result.(*File)
|
||
if !ok {
|
||
t.Fatalf("Expected *File, got %T", result)
|
||
}
|
||
|
||
if file.ContentType != "image/png" {
|
||
t.Errorf("Expected content type 'image/png', got '%s'", file.ContentType)
|
||
}
|
||
})
|
||
|
||
// Test 4: Save with options - verify via Info since File struct may not have all fields
|
||
t.Run("SaveWithOptions", func(t *testing.T) {
|
||
content := "Content with options"
|
||
base64Content := base64.StdEncoding.EncodeToString([]byte(content))
|
||
dataURI := fmt.Sprintf("data:text/plain;base64,%s", base64Content)
|
||
|
||
options := map[string]interface{}{
|
||
"groups": []interface{}{"test", "unit"},
|
||
"public": true,
|
||
"share": "team",
|
||
}
|
||
|
||
p := process.New("attachment.Save", "data.local", dataURI, "options.txt", options)
|
||
result := processSave(p)
|
||
|
||
if err, ok := result.(error); ok {
|
||
t.Fatalf("Failed to save file with options: %v", err)
|
||
}
|
||
|
||
file, ok := result.(*File)
|
||
if !ok {
|
||
t.Fatalf("Expected *File, got %T", result)
|
||
}
|
||
|
||
// File should be saved successfully
|
||
if file.ID == "" {
|
||
t.Error("File ID should not be empty")
|
||
}
|
||
|
||
// Get info to verify public and share fields
|
||
infoP := process.New("attachment.Info", "data.local", file.ID)
|
||
infoResult := processInfo(infoP)
|
||
info, ok := infoResult.(*File)
|
||
if !ok {
|
||
t.Fatalf("Failed to get file info: %v", infoResult)
|
||
}
|
||
|
||
if !info.Public {
|
||
t.Error("Expected file to be public")
|
||
}
|
||
|
||
if info.Share != "team" {
|
||
t.Errorf("Expected share 'team', got '%s'", info.Share)
|
||
}
|
||
|
||
t.Logf("Saved file with options - ID: %s, Public: %v, Share: %s", file.ID, info.Public, info.Share)
|
||
})
|
||
|
||
// Test 5: Save without filename (auto-generate)
|
||
t.Run("SaveWithoutFilename", func(t *testing.T) {
|
||
content := "Auto filename content"
|
||
base64Content := base64.StdEncoding.EncodeToString([]byte(content))
|
||
dataURI := fmt.Sprintf("data:application/json;base64,%s", base64Content)
|
||
|
||
p := process.New("attachment.Save", "data.local", dataURI)
|
||
result := processSave(p)
|
||
|
||
if err, ok := result.(error); ok {
|
||
t.Fatalf("Failed to save file: %v", err)
|
||
}
|
||
|
||
file, ok := result.(*File)
|
||
if !ok {
|
||
t.Fatalf("Expected *File, got %T", result)
|
||
}
|
||
|
||
// Should auto-generate a filename
|
||
if file.Filename == "" {
|
||
t.Error("Filename should not be empty")
|
||
}
|
||
|
||
t.Logf("Auto-generated filename: %s", file.Filename)
|
||
})
|
||
|
||
// Test 6: Save with invalid uploader
|
||
t.Run("SaveWithInvalidUploader", func(t *testing.T) {
|
||
content := "Test content"
|
||
base64Content := base64.StdEncoding.EncodeToString([]byte(content))
|
||
dataURI := fmt.Sprintf("data:text/plain;base64,%s", base64Content)
|
||
|
||
p := process.New("attachment.Save", "non-existent-uploader", dataURI, "test.txt")
|
||
result := processSave(p)
|
||
|
||
err, ok := result.(error)
|
||
if !ok {
|
||
t.Fatal("Expected error for non-existent uploader")
|
||
}
|
||
|
||
if !strings.Contains(err.Error(), "uploader not found") {
|
||
t.Errorf("Expected 'uploader not found' error, got: %s", err.Error())
|
||
}
|
||
})
|
||
|
||
// Test 7: Save with invalid base64
|
||
t.Run("SaveWithInvalidBase64", func(t *testing.T) {
|
||
invalidDataURI := "data:text/plain;base64,not-valid-base64!!!"
|
||
|
||
p := process.New("attachment.Save", "data.local", invalidDataURI, "invalid.txt")
|
||
result := processSave(p)
|
||
|
||
_, ok := result.(error)
|
||
if !ok {
|
||
t.Fatal("Expected error for invalid base64")
|
||
}
|
||
})
|
||
|
||
// Test 8: Save plain text directly (no data URI)
|
||
t.Run("SavePlainText", func(t *testing.T) {
|
||
content := "This is plain text content without data URI encoding."
|
||
|
||
p := process.New("attachment.Save", "data.local", content, "plain-text.txt")
|
||
result := processSave(p)
|
||
|
||
if err, ok := result.(error); ok {
|
||
t.Fatalf("Failed to save plain text: %v", err)
|
||
}
|
||
|
||
file, ok := result.(*File)
|
||
if !ok {
|
||
t.Fatalf("Expected *File, got %T", result)
|
||
}
|
||
|
||
if file.ID == "" {
|
||
t.Error("File ID should not be empty")
|
||
}
|
||
|
||
// Content type should be text/plain for plain text
|
||
if !strings.HasPrefix(file.ContentType, "text/plain") {
|
||
t.Errorf("Expected content type 'text/plain', got '%s'", file.ContentType)
|
||
}
|
||
|
||
// Read back and verify content
|
||
readP := process.New("attachment.Read", "data.local", file.ID)
|
||
readResult := processRead(readP)
|
||
|
||
dataURI, ok := readResult.(string)
|
||
if !ok {
|
||
t.Fatalf("Expected string, got %T: %v", readResult, readResult)
|
||
}
|
||
|
||
// Decode from data URI
|
||
parts := strings.SplitN(dataURI, ",", 2)
|
||
if len(parts) != 2 {
|
||
t.Fatalf("Invalid data URI format")
|
||
}
|
||
decoded, err := base64.StdEncoding.DecodeString(parts[1])
|
||
if err != nil {
|
||
t.Fatalf("Failed to decode: %v", err)
|
||
}
|
||
|
||
if string(decoded) != content {
|
||
t.Errorf("Content mismatch: expected %q, got %q", content, string(decoded))
|
||
}
|
||
})
|
||
|
||
// Test 9: Save Chinese text directly (UTF-8)
|
||
t.Run("SaveChineseText", func(t *testing.T) {
|
||
content := "这是一段中文内容,测试UTF-8编码。\n第二行内容。"
|
||
|
||
p := process.New("attachment.Save", "data.local", content, "chinese.txt")
|
||
result := processSave(p)
|
||
|
||
if err, ok := result.(error); ok {
|
||
t.Fatalf("Failed to save Chinese text: %v", err)
|
||
}
|
||
|
||
file, ok := result.(*File)
|
||
if !ok {
|
||
t.Fatalf("Expected *File, got %T", result)
|
||
}
|
||
|
||
// Read back and verify content
|
||
readP := process.New("attachment.Read", "data.local", file.ID)
|
||
readResult := processRead(readP)
|
||
|
||
dataURI, ok := readResult.(string)
|
||
if !ok {
|
||
t.Fatalf("Expected string, got %T: %v", readResult, readResult)
|
||
}
|
||
|
||
// Decode from data URI
|
||
parts := strings.SplitN(dataURI, ",", 2)
|
||
if len(parts) != 2 {
|
||
t.Fatalf("Invalid data URI format")
|
||
}
|
||
decoded, err := base64.StdEncoding.DecodeString(parts[1])
|
||
if err != nil {
|
||
t.Fatalf("Failed to decode: %v", err)
|
||
}
|
||
|
||
if string(decoded) != content {
|
||
t.Errorf("Chinese content mismatch: expected %q, got %q", content, string(decoded))
|
||
}
|
||
})
|
||
}
|
||
|
||
func TestProcessRead(t *testing.T) {
|
||
test.Prepare(t, config.Conf)
|
||
defer test.Clean()
|
||
|
||
// Register default uploader for testing
|
||
_, err := RegisterDefault("data.local")
|
||
if err != nil {
|
||
t.Fatalf("Failed to register manager: %v", err)
|
||
}
|
||
|
||
// First, save a file to read
|
||
content := "Content to read"
|
||
base64Content := base64.StdEncoding.EncodeToString([]byte(content))
|
||
dataURI := fmt.Sprintf("data:text/plain;base64,%s", base64Content)
|
||
|
||
saveP := process.New("attachment.Save", "data.local", dataURI, "read-test.txt")
|
||
saveResult := processSave(saveP)
|
||
file, ok := saveResult.(*File)
|
||
if !ok {
|
||
t.Fatalf("Failed to save test file: %v", saveResult)
|
||
}
|
||
|
||
// Test 1: Read file as data URI
|
||
t.Run("ReadAsDataURI", func(t *testing.T) {
|
||
p := process.New("attachment.Read", "data.local", file.ID)
|
||
result := processRead(p)
|
||
|
||
if err, ok := result.(error); ok {
|
||
t.Fatalf("Failed to read file: %v", err)
|
||
}
|
||
|
||
resultDataURI, ok := result.(string)
|
||
if !ok {
|
||
t.Fatalf("Expected string, got %T", result)
|
||
}
|
||
|
||
// Should return data URI format
|
||
if !strings.HasPrefix(resultDataURI, "data:text/plain") {
|
||
t.Errorf("Expected data URI starting with 'data:text/plain', got: %s", resultDataURI[:50])
|
||
}
|
||
|
||
if !strings.Contains(resultDataURI, ";base64,") {
|
||
t.Error("Expected data URI to contain ';base64,'")
|
||
}
|
||
|
||
// Decode and verify content
|
||
parts := strings.SplitN(resultDataURI, ",", 2)
|
||
if len(parts) != 2 {
|
||
t.Fatal("Invalid data URI format")
|
||
}
|
||
|
||
decodedContent, err := base64.StdEncoding.DecodeString(parts[1])
|
||
if err != nil {
|
||
t.Fatalf("Failed to decode base64: %v", err)
|
||
}
|
||
|
||
if string(decodedContent) != content {
|
||
t.Errorf("Content mismatch. Expected: %s, Got: %s", content, string(decodedContent))
|
||
}
|
||
|
||
t.Logf("Read file successfully - Data URI length: %d", len(resultDataURI))
|
||
})
|
||
|
||
// Test 2: Read non-existent file
|
||
t.Run("ReadNonExistent", func(t *testing.T) {
|
||
p := process.New("attachment.Read", "data.local", "non-existent-file-id")
|
||
result := processRead(p)
|
||
|
||
err, ok := result.(error)
|
||
if !ok {
|
||
t.Fatal("Expected error for non-existent file")
|
||
}
|
||
|
||
if !strings.Contains(err.Error(), "file not found") {
|
||
t.Errorf("Expected 'file not found' error, got: %s", err.Error())
|
||
}
|
||
})
|
||
|
||
// Test 3: Read with invalid uploader
|
||
t.Run("ReadWithInvalidUploader", func(t *testing.T) {
|
||
p := process.New("attachment.Read", "non-existent-uploader", file.ID)
|
||
result := processRead(p)
|
||
|
||
err, ok := result.(error)
|
||
if !ok {
|
||
t.Fatal("Expected error for non-existent uploader")
|
||
}
|
||
|
||
if !strings.Contains(err.Error(), "uploader not found") {
|
||
t.Errorf("Expected 'uploader not found' error, got: %s", err.Error())
|
||
}
|
||
})
|
||
}
|
||
|
||
func TestProcessInfo(t *testing.T) {
|
||
test.Prepare(t, config.Conf)
|
||
defer test.Clean()
|
||
|
||
// Register default uploader for testing
|
||
_, err := RegisterDefault("data.local")
|
||
if err != nil {
|
||
t.Fatalf("Failed to register manager: %v", err)
|
||
}
|
||
|
||
// Save a file with options
|
||
content := "Info test content"
|
||
base64Content := base64.StdEncoding.EncodeToString([]byte(content))
|
||
dataURI := fmt.Sprintf("data:text/plain;base64,%s", base64Content)
|
||
|
||
options := map[string]interface{}{
|
||
"groups": []interface{}{"info", "test"},
|
||
"public": true,
|
||
"share": "team",
|
||
}
|
||
|
||
saveP := process.New("attachment.Save", "data.local", dataURI, "info-test.txt", options)
|
||
saveResult := processSave(saveP)
|
||
file, ok := saveResult.(*File)
|
||
if !ok {
|
||
t.Fatalf("Failed to save test file: %v", saveResult)
|
||
}
|
||
|
||
// Test 1: Get file info
|
||
t.Run("GetFileInfo", func(t *testing.T) {
|
||
p := process.New("attachment.Info", "data.local", file.ID)
|
||
result := processInfo(p)
|
||
|
||
if err, ok := result.(error); ok {
|
||
t.Fatalf("Failed to get file info: %v", err)
|
||
}
|
||
|
||
info, ok := result.(*File)
|
||
if !ok {
|
||
t.Fatalf("Expected *File, got %T", result)
|
||
}
|
||
|
||
if info.ID != file.ID {
|
||
t.Errorf("Expected ID %s, got %s", file.ID, info.ID)
|
||
}
|
||
|
||
if info.Filename != file.Filename {
|
||
t.Errorf("Expected filename %s, got %s", file.Filename, info.Filename)
|
||
}
|
||
|
||
if !strings.HasPrefix(info.ContentType, "text/plain") {
|
||
t.Errorf("Expected content type 'text/plain', got %s", info.ContentType)
|
||
}
|
||
|
||
if !info.Public {
|
||
t.Error("Expected file to be public")
|
||
}
|
||
|
||
if info.Share != "team" {
|
||
t.Errorf("Expected share 'team', got %s", info.Share)
|
||
}
|
||
|
||
t.Logf("File info - ID: %s, Filename: %s, Bytes: %d, Public: %v, Share: %s",
|
||
info.ID, info.Filename, info.Bytes, info.Public, info.Share)
|
||
})
|
||
|
||
// Test 2: Get info for non-existent file
|
||
t.Run("GetInfoNonExistent", func(t *testing.T) {
|
||
p := process.New("attachment.Info", "data.local", "non-existent-file-id")
|
||
result := processInfo(p)
|
||
|
||
err, ok := result.(error)
|
||
if !ok {
|
||
t.Fatal("Expected error for non-existent file")
|
||
}
|
||
|
||
if !strings.Contains(err.Error(), "file not found") {
|
||
t.Errorf("Expected 'file not found' error, got: %s", err.Error())
|
||
}
|
||
})
|
||
}
|
||
|
||
func TestProcessList(t *testing.T) {
|
||
test.Prepare(t, config.Conf)
|
||
defer test.Clean()
|
||
|
||
// Use unique manager name for test isolation
|
||
managerName := fmt.Sprintf("data.local.list.%d", time.Now().UnixNano())
|
||
_, err := RegisterDefault(managerName)
|
||
if err != nil {
|
||
t.Fatalf("Failed to register manager: %v", err)
|
||
}
|
||
|
||
// Upload multiple test files
|
||
testFiles := []struct {
|
||
content string
|
||
filename string
|
||
contentType string
|
||
}{
|
||
{"File 1 content", "file1.txt", "text/plain"},
|
||
{"File 2 content", "file2.txt", "text/plain"},
|
||
{"File 3 content", "file3.txt", "text/plain"},
|
||
{`{"key": "value"}`, "data.json", "application/json"},
|
||
{"CSV,Data\n1,2", "data.csv", "text/csv"},
|
||
}
|
||
|
||
uploadedIDs := make([]string, 0, len(testFiles))
|
||
for _, tf := range testFiles {
|
||
base64Content := base64.StdEncoding.EncodeToString([]byte(tf.content))
|
||
dataURI := fmt.Sprintf("data:%s;base64,%s", tf.contentType, base64Content)
|
||
|
||
p := process.New("attachment.Save", managerName, dataURI, tf.filename)
|
||
result := processSave(p)
|
||
|
||
file, ok := result.(*File)
|
||
if !ok {
|
||
t.Fatalf("Failed to save file %s: %v", tf.filename, result)
|
||
}
|
||
uploadedIDs = append(uploadedIDs, file.ID)
|
||
}
|
||
|
||
// Test 1: Basic list
|
||
t.Run("BasicList", func(t *testing.T) {
|
||
p := process.New("attachment.List", managerName)
|
||
result := processList(p)
|
||
|
||
if err, ok := result.(error); ok {
|
||
t.Fatalf("Failed to list files: %v", err)
|
||
}
|
||
|
||
listResult, ok := result.(*ListResult)
|
||
if !ok {
|
||
t.Fatalf("Expected *ListResult, got %T", result)
|
||
}
|
||
|
||
if len(listResult.Files) != len(testFiles) {
|
||
t.Errorf("Expected %d files, got %d", len(testFiles), len(listResult.Files))
|
||
}
|
||
|
||
if listResult.Total != int64(len(testFiles)) {
|
||
t.Errorf("Expected total %d, got %d", len(testFiles), listResult.Total)
|
||
}
|
||
|
||
t.Logf("List result - Total: %d, Page: %d, PageSize: %d", listResult.Total, listResult.Page, listResult.PageSize)
|
||
})
|
||
|
||
// Test 2: List with pagination
|
||
t.Run("ListWithPagination", func(t *testing.T) {
|
||
options := map[string]interface{}{
|
||
"page": 1,
|
||
"page_size": 2,
|
||
}
|
||
|
||
p := process.New("attachment.List", managerName, options)
|
||
result := processList(p)
|
||
|
||
if err, ok := result.(error); ok {
|
||
t.Fatalf("Failed to list files with pagination: %v", err)
|
||
}
|
||
|
||
listResult, ok := result.(*ListResult)
|
||
if !ok {
|
||
t.Fatalf("Expected *ListResult, got %T", result)
|
||
}
|
||
|
||
if len(listResult.Files) != 2 {
|
||
t.Errorf("Expected 2 files, got %d", len(listResult.Files))
|
||
}
|
||
|
||
if listResult.PageSize != 2 {
|
||
t.Errorf("Expected page size 2, got %d", listResult.PageSize)
|
||
}
|
||
|
||
if listResult.TotalPages != 3 { // 5 files / 2 per page = 3 pages
|
||
t.Errorf("Expected 3 total pages, got %d", listResult.TotalPages)
|
||
}
|
||
})
|
||
|
||
// Test 3: List with filters - use content_type wildcard
|
||
t.Run("ListWithFilters", func(t *testing.T) {
|
||
options := map[string]interface{}{
|
||
"filters": map[string]interface{}{
|
||
"content_type": "text/*",
|
||
},
|
||
}
|
||
|
||
p := process.New("attachment.List", managerName, options)
|
||
result := processList(p)
|
||
|
||
if err, ok := result.(error); ok {
|
||
t.Fatalf("Failed to list files with filters: %v", err)
|
||
}
|
||
|
||
listResult, ok := result.(*ListResult)
|
||
if !ok {
|
||
t.Fatalf("Expected *ListResult, got %T", result)
|
||
}
|
||
|
||
// Should find text/plain and text/csv files
|
||
// Note: The filter implementation may vary, so we just check the call succeeds
|
||
t.Logf("List with content_type filter - Total: %d files", listResult.Total)
|
||
})
|
||
|
||
// Test 4: List with ordering
|
||
t.Run("ListWithOrdering", func(t *testing.T) {
|
||
options := map[string]interface{}{
|
||
"order_by": "name asc",
|
||
}
|
||
|
||
p := process.New("attachment.List", managerName, options)
|
||
result := processList(p)
|
||
|
||
if err, ok := result.(error); ok {
|
||
t.Fatalf("Failed to list files with ordering: %v", err)
|
||
}
|
||
|
||
listResult, ok := result.(*ListResult)
|
||
if !ok {
|
||
t.Fatalf("Expected *ListResult, got %T", result)
|
||
}
|
||
|
||
// Verify files are sorted
|
||
for i := 1; i < len(listResult.Files); i++ {
|
||
if listResult.Files[i-1].Filename > listResult.Files[i].Filename {
|
||
t.Errorf("Files are not sorted ascending by name")
|
||
break
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
func TestProcessDelete(t *testing.T) {
|
||
test.Prepare(t, config.Conf)
|
||
defer test.Clean()
|
||
|
||
// Register default uploader for testing
|
||
_, err := RegisterDefault("data.local")
|
||
if err != nil {
|
||
t.Fatalf("Failed to register manager: %v", err)
|
||
}
|
||
|
||
// Save a file to delete
|
||
content := "Content to delete"
|
||
base64Content := base64.StdEncoding.EncodeToString([]byte(content))
|
||
dataURI := fmt.Sprintf("data:text/plain;base64,%s", base64Content)
|
||
|
||
saveP := process.New("attachment.Save", "data.local", dataURI, "delete-test.txt")
|
||
saveResult := processSave(saveP)
|
||
file, ok := saveResult.(*File)
|
||
if !ok {
|
||
t.Fatalf("Failed to save test file: %v", saveResult)
|
||
}
|
||
|
||
// Test 1: Delete existing file
|
||
t.Run("DeleteExistingFile", func(t *testing.T) {
|
||
// Verify file exists first
|
||
existsP := process.New("attachment.Exists", "data.local", file.ID)
|
||
existsResult := processExists(existsP)
|
||
if exists, ok := existsResult.(bool); !ok || !exists {
|
||
t.Fatal("File should exist before deletion")
|
||
}
|
||
|
||
// Delete the file
|
||
p := process.New("attachment.Delete", "data.local", file.ID)
|
||
result := processDelete(p)
|
||
|
||
if err, ok := result.(error); ok {
|
||
t.Fatalf("Failed to delete file: %v", err)
|
||
}
|
||
|
||
success, ok := result.(bool)
|
||
if !ok || !success {
|
||
t.Errorf("Expected true, got %v", result)
|
||
}
|
||
|
||
// Verify file no longer exists
|
||
existsP2 := process.New("attachment.Exists", "data.local", file.ID)
|
||
existsResult2 := processExists(existsP2)
|
||
if exists, ok := existsResult2.(bool); ok && exists {
|
||
t.Error("File should not exist after deletion")
|
||
}
|
||
})
|
||
|
||
// Test 2: Delete non-existent file
|
||
t.Run("DeleteNonExistent", func(t *testing.T) {
|
||
p := process.New("attachment.Delete", "data.local", "non-existent-file-id")
|
||
result := processDelete(p)
|
||
|
||
err, ok := result.(error)
|
||
if !ok {
|
||
t.Fatal("Expected error for non-existent file")
|
||
}
|
||
|
||
if !strings.Contains(err.Error(), "file not found") {
|
||
t.Errorf("Expected 'file not found' error, got: %s", err.Error())
|
||
}
|
||
})
|
||
}
|
||
|
||
func TestProcessExists(t *testing.T) {
|
||
test.Prepare(t, config.Conf)
|
||
defer test.Clean()
|
||
|
||
// Register default uploader for testing
|
||
_, err := RegisterDefault("data.local")
|
||
if err != nil {
|
||
t.Fatalf("Failed to register manager: %v", err)
|
||
}
|
||
|
||
// Save a file
|
||
content := "Exists test content"
|
||
base64Content := base64.StdEncoding.EncodeToString([]byte(content))
|
||
dataURI := fmt.Sprintf("data:text/plain;base64,%s", base64Content)
|
||
|
||
saveP := process.New("attachment.Save", "data.local", dataURI, "exists-test.txt")
|
||
saveResult := processSave(saveP)
|
||
file, ok := saveResult.(*File)
|
||
if !ok {
|
||
t.Fatalf("Failed to save test file: %v", saveResult)
|
||
}
|
||
|
||
// Test 1: Existing file
|
||
t.Run("FileExists", func(t *testing.T) {
|
||
p := process.New("attachment.Exists", "data.local", file.ID)
|
||
result := processExists(p)
|
||
|
||
exists, ok := result.(bool)
|
||
if !ok {
|
||
t.Fatalf("Expected bool, got %T", result)
|
||
}
|
||
|
||
if !exists {
|
||
t.Error("File should exist")
|
||
}
|
||
})
|
||
|
||
// Test 2: Non-existent file
|
||
t.Run("FileNotExists", func(t *testing.T) {
|
||
p := process.New("attachment.Exists", "data.local", "non-existent-file-id")
|
||
result := processExists(p)
|
||
|
||
exists, ok := result.(bool)
|
||
if !ok {
|
||
t.Fatalf("Expected bool, got %T", result)
|
||
}
|
||
|
||
if exists {
|
||
t.Error("File should not exist")
|
||
}
|
||
})
|
||
}
|
||
|
||
func TestProcessURL(t *testing.T) {
|
||
test.Prepare(t, config.Conf)
|
||
defer test.Clean()
|
||
|
||
// Register default uploader for testing
|
||
_, err := RegisterDefault("data.local")
|
||
if err != nil {
|
||
t.Fatalf("Failed to register manager: %v", err)
|
||
}
|
||
|
||
// Save a file
|
||
content := "URL test content"
|
||
base64Content := base64.StdEncoding.EncodeToString([]byte(content))
|
||
dataURI := fmt.Sprintf("data:text/plain;base64,%s", base64Content)
|
||
|
||
saveP := process.New("attachment.Save", "data.local", dataURI, "url-test.txt")
|
||
saveResult := processSave(saveP)
|
||
file, ok := saveResult.(*File)
|
||
if !ok {
|
||
t.Fatalf("Failed to save test file: %v", saveResult)
|
||
}
|
||
|
||
// Test 1: Get URL
|
||
t.Run("GetURL", func(t *testing.T) {
|
||
p := process.New("attachment.URL", "data.local", file.ID)
|
||
result := processURL(p)
|
||
|
||
if err, ok := result.(error); ok {
|
||
t.Fatalf("Failed to get URL: %v", err)
|
||
}
|
||
|
||
url, ok := result.(string)
|
||
if !ok {
|
||
t.Fatalf("Expected string, got %T", result)
|
||
}
|
||
|
||
if url == "" {
|
||
t.Error("URL should not be empty")
|
||
}
|
||
|
||
t.Logf("File URL: %s", url)
|
||
})
|
||
|
||
// Test 2: Get URL for non-existent file
|
||
t.Run("GetURLNonExistent", func(t *testing.T) {
|
||
p := process.New("attachment.URL", "data.local", "non-existent-file-id")
|
||
result := processURL(p)
|
||
|
||
err, ok := result.(error)
|
||
if !ok {
|
||
t.Fatal("Expected error for non-existent file")
|
||
}
|
||
|
||
if !strings.Contains(err.Error(), "file not found") {
|
||
t.Errorf("Expected 'file not found' error, got: %s", err.Error())
|
||
}
|
||
})
|
||
}
|
||
|
||
func TestProcessSaveTextAndGetText(t *testing.T) {
|
||
test.Prepare(t, config.Conf)
|
||
defer test.Clean()
|
||
|
||
// Register default uploader for testing
|
||
_, err := RegisterDefault("data.local")
|
||
if err != nil {
|
||
t.Fatalf("Failed to register manager: %v", err)
|
||
}
|
||
|
||
// Save a file
|
||
content := "Original file content"
|
||
base64Content := base64.StdEncoding.EncodeToString([]byte(content))
|
||
dataURI := fmt.Sprintf("data:text/plain;base64,%s", base64Content)
|
||
|
||
saveP := process.New("attachment.Save", "data.local", dataURI, "text-test.txt")
|
||
saveResult := processSave(saveP)
|
||
file, ok := saveResult.(*File)
|
||
if !ok {
|
||
t.Fatalf("Failed to save test file: %v", saveResult)
|
||
}
|
||
|
||
// Test 1: Get text from file without saved text (should be empty)
|
||
t.Run("GetTextEmpty", func(t *testing.T) {
|
||
p := process.New("attachment.GetText", "data.local", file.ID)
|
||
result := processGetText(p)
|
||
|
||
if err, ok := result.(error); ok {
|
||
t.Fatalf("Failed to get text: %v", err)
|
||
}
|
||
|
||
text, ok := result.(string)
|
||
if !ok {
|
||
t.Fatalf("Expected string, got %T", result)
|
||
}
|
||
|
||
if text != "" {
|
||
t.Errorf("Expected empty text, got: %s", text)
|
||
}
|
||
})
|
||
|
||
// Test 2: Save text and retrieve
|
||
t.Run("SaveTextAndRetrieve", func(t *testing.T) {
|
||
parsedText := "This is the parsed/extracted text content from the file."
|
||
|
||
// Save text
|
||
saveTextP := process.New("attachment.SaveText", "data.local", file.ID, parsedText)
|
||
saveTextResult := processSaveText(saveTextP)
|
||
|
||
if err, ok := saveTextResult.(error); ok {
|
||
t.Fatalf("Failed to save text: %v", err)
|
||
}
|
||
|
||
success, ok := saveTextResult.(bool)
|
||
if !ok || !success {
|
||
t.Errorf("Expected true, got %v", saveTextResult)
|
||
}
|
||
|
||
// Retrieve text
|
||
getTextP := process.New("attachment.GetText", "data.local", file.ID)
|
||
getTextResult := processGetText(getTextP)
|
||
|
||
if err, ok := getTextResult.(error); ok {
|
||
t.Fatalf("Failed to get text: %v", err)
|
||
}
|
||
|
||
retrievedText, ok := getTextResult.(string)
|
||
if !ok {
|
||
t.Fatalf("Expected string, got %T", getTextResult)
|
||
}
|
||
|
||
if retrievedText != parsedText {
|
||
t.Errorf("Text mismatch. Expected: %s, Got: %s", parsedText, retrievedText)
|
||
}
|
||
|
||
t.Logf("Saved and retrieved text: %s", retrievedText)
|
||
})
|
||
|
||
// Test 3: Get full text vs preview
|
||
t.Run("GetTextFullVsPreview", func(t *testing.T) {
|
||
// Save a long text
|
||
longText := strings.Repeat("This is a long text content. ", 200) // > 2000 chars
|
||
|
||
saveTextP := process.New("attachment.SaveText", "data.local", file.ID, longText)
|
||
saveTextResult := processSaveText(saveTextP)
|
||
if err, ok := saveTextResult.(error); ok {
|
||
t.Fatalf("Failed to save long text: %v", err)
|
||
}
|
||
|
||
// Get preview (default)
|
||
previewP := process.New("attachment.GetText", "data.local", file.ID)
|
||
previewResult := processGetText(previewP)
|
||
previewText, _ := previewResult.(string)
|
||
|
||
// Preview should be 2000 runes
|
||
if len([]rune(previewText)) != 2000 {
|
||
t.Errorf("Preview should be 2000 runes, got %d", len([]rune(previewText)))
|
||
}
|
||
|
||
// Get full content
|
||
fullP := process.New("attachment.GetText", "data.local", file.ID, true)
|
||
fullResult := processGetText(fullP)
|
||
fullText, _ := fullResult.(string)
|
||
|
||
if fullText != longText {
|
||
t.Errorf("Full text length mismatch. Expected: %d, Got: %d", len(longText), len(fullText))
|
||
}
|
||
})
|
||
|
||
// Test 4: Save/Get text for non-existent file
|
||
t.Run("SaveTextNonExistent", func(t *testing.T) {
|
||
p := process.New("attachment.SaveText", "data.local", "non-existent-id", "some text")
|
||
result := processSaveText(p)
|
||
|
||
err, ok := result.(error)
|
||
if !ok {
|
||
t.Fatal("Expected error for non-existent file")
|
||
}
|
||
|
||
if !strings.Contains(err.Error(), "file not found") {
|
||
t.Errorf("Expected 'file not found' error, got: %s", err.Error())
|
||
}
|
||
})
|
||
}
|
||
|
||
func TestProcessWithAuthorizedPermission(t *testing.T) {
|
||
test.Prepare(t, config.Conf)
|
||
defer test.Clean()
|
||
|
||
// Register default uploader for testing
|
||
_, err := RegisterDefault("data.local")
|
||
if err != nil {
|
||
t.Fatalf("Failed to register manager: %v", err)
|
||
}
|
||
|
||
// Test 1: Save with Authorized info - verify via database query since File struct
|
||
// does not expose these fields in JSON (they are marked with json:"-")
|
||
t.Run("SaveWithAuthorizedInfo", func(t *testing.T) {
|
||
content := "Content with permission"
|
||
base64Content := base64.StdEncoding.EncodeToString([]byte(content))
|
||
dataURI := fmt.Sprintf("data:text/plain;base64,%s", base64Content)
|
||
|
||
p := process.New("attachment.Save", "data.local", dataURI, "perm-test.txt")
|
||
|
||
// Set authorized info
|
||
p.WithAuthorized(process.AuthorizedInfo{
|
||
UserID: "user123",
|
||
TeamID: "team456",
|
||
TenantID: "tenant789",
|
||
})
|
||
|
||
result := processSave(p)
|
||
|
||
if err, ok := result.(error); ok {
|
||
t.Fatalf("Failed to save file: %v", err)
|
||
}
|
||
|
||
file, ok := result.(*File)
|
||
if !ok {
|
||
t.Fatalf("Expected *File, got %T", result)
|
||
}
|
||
|
||
// File should be saved successfully
|
||
if file.ID == "" {
|
||
t.Error("File ID should not be empty")
|
||
}
|
||
|
||
// Note: The YaoCreatedBy, YaoTeamID, YaoTenantID fields in File struct
|
||
// are marked with json:"-" and may not be populated in the returned struct.
|
||
// The permission fields are stored in the database during upload via UploadOption.
|
||
// To verify, we would need to query the database directly.
|
||
t.Logf("File saved with authorized info - ID: %s", file.ID)
|
||
})
|
||
|
||
// Test 2: Save without Authorized (should still work)
|
||
t.Run("SaveWithoutAuthorized", func(t *testing.T) {
|
||
content := "Content without permission"
|
||
base64Content := base64.StdEncoding.EncodeToString([]byte(content))
|
||
dataURI := fmt.Sprintf("data:text/plain;base64,%s", base64Content)
|
||
|
||
p := process.New("attachment.Save", "data.local", dataURI, "no-perm-test.txt")
|
||
// Don't set authorized info
|
||
|
||
result := processSave(p)
|
||
|
||
if err, ok := result.(error); ok {
|
||
t.Fatalf("Failed to save file: %v", err)
|
||
}
|
||
|
||
file, ok := result.(*File)
|
||
if !ok {
|
||
t.Fatalf("Expected *File, got %T", result)
|
||
}
|
||
|
||
// File should be saved successfully without permission fields
|
||
if file.ID == "" {
|
||
t.Error("File ID should not be empty")
|
||
}
|
||
|
||
t.Logf("File saved without permissions - ID: %s", file.ID)
|
||
})
|
||
}
|
||
|
||
func TestParseDataURI(t *testing.T) {
|
||
// Test 1: Valid data URI with content type
|
||
t.Run("ValidDataURI", func(t *testing.T) {
|
||
content := "Hello, World!"
|
||
base64Content := base64.StdEncoding.EncodeToString([]byte(content))
|
||
dataURI := fmt.Sprintf("data:text/plain;base64,%s", base64Content)
|
||
|
||
contentType, data, err := parseDataURI(dataURI)
|
||
if err != nil {
|
||
t.Fatalf("Failed to parse data URI: %v", err)
|
||
}
|
||
|
||
if contentType != "text/plain" {
|
||
t.Errorf("Expected content type 'text/plain', got '%s'", contentType)
|
||
}
|
||
|
||
if string(data) != content {
|
||
t.Errorf("Expected content '%s', got '%s'", content, string(data))
|
||
}
|
||
})
|
||
|
||
// Test 2: Plain text (no data URI header) - treated as plain text, not base64
|
||
t.Run("PlainText", func(t *testing.T) {
|
||
content := "Plain text content"
|
||
|
||
contentType, data, err := parseDataURI(content)
|
||
if err != nil {
|
||
t.Fatalf("Failed to parse plain text: %v", err)
|
||
}
|
||
|
||
if contentType != "text/plain" {
|
||
t.Errorf("Expected content type 'text/plain', got '%s'", contentType)
|
||
}
|
||
|
||
if string(data) != content {
|
||
t.Errorf("Expected content '%s', got '%s'", content, string(data))
|
||
}
|
||
})
|
||
|
||
// Test 3: Data URI with image
|
||
t.Run("ImageDataURI", func(t *testing.T) {
|
||
pngBase64 := "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
|
||
dataURI := fmt.Sprintf("data:image/png;base64,%s", pngBase64)
|
||
|
||
contentType, _, err := parseDataURI(dataURI)
|
||
if err != nil {
|
||
t.Fatalf("Failed to parse image data URI: %v", err)
|
||
}
|
||
|
||
if contentType != "image/png" {
|
||
t.Errorf("Expected content type 'image/png', got '%s'", contentType)
|
||
}
|
||
})
|
||
|
||
// Test 4: Invalid base64
|
||
t.Run("InvalidBase64", func(t *testing.T) {
|
||
dataURI := "data:text/plain;base64,not-valid!!!"
|
||
|
||
_, _, err := parseDataURI(dataURI)
|
||
if err == nil {
|
||
t.Fatal("Expected error for invalid base64")
|
||
}
|
||
})
|
||
|
||
// Test 5: Invalid data URI format
|
||
t.Run("InvalidDataURIFormat", func(t *testing.T) {
|
||
dataURI := "data:text/plain" // Missing base64 part
|
||
|
||
_, _, err := parseDataURI(dataURI)
|
||
if err == nil {
|
||
t.Fatal("Expected error for invalid data URI format")
|
||
}
|
||
})
|
||
}
|
||
|
||
func TestGenerateFilename(t *testing.T) {
|
||
// Note: mime.ExtensionsByType may return different extensions on different systems
|
||
// (e.g., Linux may return .jfif for image/jpeg, .asc for text/plain)
|
||
// So we verify the filename has a proper extension format and is not empty
|
||
testCases := []struct {
|
||
contentType string
|
||
expectedPrefix string
|
||
}{
|
||
{"image/png", "file"},
|
||
{"image/jpeg", "file"},
|
||
{"image/gif", "file"},
|
||
{"image/webp", "file"},
|
||
{"text/plain", "file"},
|
||
{"application/pdf", "file"},
|
||
{"application/json", "file"},
|
||
{"application/octet-stream", "file"},
|
||
{"unknown/type", "file"},
|
||
}
|
||
|
||
for _, tc := range testCases {
|
||
t.Run(tc.contentType, func(t *testing.T) {
|
||
filename := generateFilename(tc.contentType)
|
||
|
||
// Check prefix
|
||
if !strings.HasPrefix(filename, tc.expectedPrefix) {
|
||
t.Errorf("For content type '%s', expected prefix '%s', got '%s'", tc.contentType, tc.expectedPrefix, filename)
|
||
}
|
||
|
||
// Check filename has an extension (starts with dot and has at least one character)
|
||
dotIndex := strings.LastIndex(filename, ".")
|
||
if dotIndex == -1 || dotIndex == len(filename)-1 {
|
||
t.Errorf("For content type '%s', expected filename with extension, got '%s'", tc.contentType, filename)
|
||
}
|
||
|
||
// Extension should not be empty
|
||
ext := filename[dotIndex:]
|
||
if len(ext) < 2 {
|
||
t.Errorf("For content type '%s', expected non-empty extension, got '%s'", tc.contentType, ext)
|
||
}
|
||
})
|
||
}
|
||
}
|