mirror of
https://github.com/langgenius/dify.git
synced 2026-08-24 12:32:54 -04:00
365 lines
12 KiB
Go
365 lines
12 KiB
Go
package agentcli
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
type fakeFileUploadClient struct {
|
|
forFrontend bool
|
|
downloadRequestCall int
|
|
downloadMethod string
|
|
uploadResponse []byte
|
|
calls []string
|
|
filename string
|
|
mimetype string
|
|
uploadURL string
|
|
uploadedBytes []byte
|
|
downloadReference string
|
|
downloadErr error
|
|
downloadURL *string
|
|
}
|
|
|
|
func (f *fakeFileUploadClient) CreateToolFileUploadURL(_ context.Context, filename, mimetype string) (string, error) {
|
|
f.calls = append(f.calls, "upload-request")
|
|
f.filename = filename
|
|
f.mimetype = mimetype
|
|
return "https://sandbox-files.example.com/files/upload/for-plugin?sign=1", nil
|
|
}
|
|
|
|
func (f *fakeFileUploadClient) UploadFileToURL(uploadURL, filePath, filename, mimetype string) ([]byte, error) {
|
|
f.calls = append(f.calls, "multipart-upload")
|
|
f.uploadURL = uploadURL
|
|
f.filename = filename
|
|
f.mimetype = mimetype
|
|
f.uploadedBytes, _ = os.ReadFile(filePath)
|
|
if f.uploadResponse != nil {
|
|
return f.uploadResponse, nil
|
|
}
|
|
return []byte(`{"reference":"dify-file-ref:eyJyZWNvcmRfaWQiOiJ0b29sLTEifQ=="}`), nil
|
|
}
|
|
|
|
func (f *fakeFileUploadClient) CreateFileDownloadURL(
|
|
_ context.Context,
|
|
transferMethod string,
|
|
reference, _ *string,
|
|
forFrontend bool,
|
|
) (*FileDownloadResponse, error) {
|
|
f.calls = append(f.calls, "download-request")
|
|
f.forFrontend = forFrontend
|
|
f.downloadMethod = transferMethod
|
|
f.downloadRequestCall++
|
|
if reference != nil {
|
|
f.downloadReference = *reference
|
|
}
|
|
if f.downloadErr != nil {
|
|
return nil, f.downloadErr
|
|
}
|
|
downloadURL := "/files/tools/report.pdf?sign=2"
|
|
if f.downloadURL != nil {
|
|
downloadURL = *f.downloadURL
|
|
}
|
|
return &FileDownloadResponse{
|
|
Filename: "report.pdf",
|
|
MimeType: "application/pdf",
|
|
Size: 123,
|
|
DownloadURL: downloadURL,
|
|
}, nil
|
|
}
|
|
|
|
func TestRunFileUploadReturnsFrontendDisplayURL(t *testing.T) {
|
|
filePath := t.TempDir() + "/report.pdf"
|
|
if err := os.WriteFile(filePath, []byte("report"), 0o600); err != nil {
|
|
t.Fatalf("write fixture: %v", err)
|
|
}
|
|
|
|
client := &fakeFileUploadClient{}
|
|
var output bytes.Buffer
|
|
if err := runFileUpload(client, filePath, false, &output); err != nil {
|
|
t.Fatalf("run file upload: %v", err)
|
|
}
|
|
|
|
if !client.forFrontend {
|
|
t.Fatal("download request did not select frontend display URL")
|
|
}
|
|
if got, want := strings.Join(client.calls, ","), "upload-request,multipart-upload,download-request"; got != want {
|
|
t.Fatalf("call order = %s, want %s", got, want)
|
|
}
|
|
if client.filename != "report.pdf" || client.mimetype != "application/pdf" {
|
|
t.Fatalf("upload metadata = (%q, %q), want report.pdf/application/pdf", client.filename, client.mimetype)
|
|
}
|
|
if client.uploadURL != "https://sandbox-files.example.com/files/upload/for-plugin?sign=1" {
|
|
t.Fatalf("upload URL = %q", client.uploadURL)
|
|
}
|
|
if string(client.uploadedBytes) != "report" {
|
|
t.Fatalf("uploaded bytes = %q, want report", client.uploadedBytes)
|
|
}
|
|
if client.downloadReference != "dify-file-ref:eyJyZWNvcmRfaWQiOiJ0b29sLTEifQ==" {
|
|
t.Fatalf("download reference = %q", client.downloadReference)
|
|
}
|
|
got := strings.TrimSpace(output.String())
|
|
want := `{"transfer_method":"tool_file","reference":"dify-file-ref:eyJyZWNvcmRfaWQiOiJ0b29sLTEifQ==","public_download_url":"/files/tools/report.pdf?sign=2"}`
|
|
if got != want {
|
|
t.Fatalf("output = %s, want %s", got, want)
|
|
}
|
|
}
|
|
|
|
func TestRunFileUploadWithoutDownloadLinkReturnsOnlyCanonicalMapping(t *testing.T) {
|
|
filePath := filepath.Join(t.TempDir(), "report.pdf")
|
|
if err := os.WriteFile(filePath, []byte("report"), 0o600); err != nil {
|
|
t.Fatalf("write fixture: %v", err)
|
|
}
|
|
|
|
client := &fakeFileUploadClient{}
|
|
var output bytes.Buffer
|
|
if err := runFileUpload(client, filePath, true, &output); err != nil {
|
|
t.Fatalf("run file upload: %v", err)
|
|
}
|
|
|
|
if client.downloadRequestCall != 0 {
|
|
t.Fatalf("download request calls = %d, want 0", client.downloadRequestCall)
|
|
}
|
|
if got, want := strings.Join(client.calls, ","), "upload-request,multipart-upload"; got != want {
|
|
t.Fatalf("call order = %s, want %s", got, want)
|
|
}
|
|
got := strings.TrimSpace(output.String())
|
|
want := `{"transfer_method":"tool_file","reference":"dify-file-ref:eyJyZWNvcmRfaWQiOiJ0b29sLTEifQ=="}`
|
|
if got != want {
|
|
t.Fatalf("output = %s, want %s", got, want)
|
|
}
|
|
}
|
|
|
|
func TestRunFileUploadPreservesReferenceWhenPublicURLRequestFails(t *testing.T) {
|
|
filePath := filepath.Join(t.TempDir(), "report.pdf")
|
|
if err := os.WriteFile(filePath, []byte("report"), 0o600); err != nil {
|
|
t.Fatalf("write fixture: %v", err)
|
|
}
|
|
|
|
client := &fakeFileUploadClient{downloadErr: &agentStubHTTPError{
|
|
statusCode: http.StatusUnauthorized,
|
|
code: agentStubAuthorizationExpiredCode,
|
|
message: "expired",
|
|
}}
|
|
var output bytes.Buffer
|
|
err := runFileUpload(client, filePath, false, &output)
|
|
if err == nil {
|
|
t.Fatal("run file upload succeeded, want public URL failure")
|
|
}
|
|
const reference = "dify-file-ref:eyJyZWNvcmRfaWQiOiJ0b29sLTEifQ=="
|
|
if got, want := strings.TrimSpace(output.String()), `{"transfer_method":"tool_file","reference":"`+reference+`"}`; got != want {
|
|
t.Fatalf("partial output = %s, want %s", got, want)
|
|
}
|
|
for _, want := range []string{
|
|
"request public download URL",
|
|
"expired after 5 minutes",
|
|
"will not refresh automatically",
|
|
"start a new shell tool call",
|
|
"retry the command",
|
|
} {
|
|
if !strings.Contains(err.Error(), want) {
|
|
t.Errorf("error = %q, want substring %q", err, want)
|
|
}
|
|
}
|
|
recoveryCommand := "dify-agent file public-url '" + reference + "'"
|
|
if !strings.HasSuffix(err.Error(), "retry without uploading again: "+recoveryCommand) {
|
|
t.Fatalf("error = %q, want exact recovery command %q", err, recoveryCommand)
|
|
}
|
|
if got, want := strings.Join(client.calls, ","), "upload-request,multipart-upload,download-request"; got != want {
|
|
t.Fatalf("call order = %s, want %s", got, want)
|
|
}
|
|
}
|
|
|
|
func TestRunFileUploadPreservesReferenceWhenPublicURLResponseIsIncomplete(t *testing.T) {
|
|
filePath := filepath.Join(t.TempDir(), "report.pdf")
|
|
if err := os.WriteFile(filePath, []byte("report"), 0o600); err != nil {
|
|
t.Fatalf("write fixture: %v", err)
|
|
}
|
|
|
|
emptyURL := ""
|
|
client := &fakeFileUploadClient{downloadURL: &emptyURL}
|
|
var output bytes.Buffer
|
|
err := runFileUpload(client, filePath, false, &output)
|
|
if err == nil || !strings.Contains(err.Error(), "missing download_url") {
|
|
t.Fatalf("error = %v, want incomplete public URL response", err)
|
|
}
|
|
if !strings.Contains(output.String(), `"reference":"dify-file-ref:`) {
|
|
t.Fatalf("partial output = %q, want uploaded reference", output.String())
|
|
}
|
|
if !strings.Contains(err.Error(), "dify-agent file public-url") {
|
|
t.Fatalf("error = %q, want recovery command", err)
|
|
}
|
|
}
|
|
|
|
func TestRunFilePublicURLUsesExistingReferenceWithoutUploading(t *testing.T) {
|
|
client := &fakeFileUploadClient{}
|
|
var output bytes.Buffer
|
|
const reference = "dify-file-ref:eyJyZWNvcmRfaWQiOiJ0b29sLTEifQ=="
|
|
|
|
if err := runFilePublicURL(client, reference, &output); err != nil {
|
|
t.Fatalf("run file public URL: %v", err)
|
|
}
|
|
|
|
if got, want := strings.Join(client.calls, ","), "download-request"; got != want {
|
|
t.Fatalf("calls = %s, want %s", got, want)
|
|
}
|
|
if !client.forFrontend || client.downloadMethod != "tool_file" || client.downloadReference != reference {
|
|
t.Fatalf(
|
|
"download request = (method=%q, forFrontend=%t, reference=%q)",
|
|
client.downloadMethod,
|
|
client.forFrontend,
|
|
client.downloadReference,
|
|
)
|
|
}
|
|
want := `{"transfer_method":"tool_file","reference":"` + reference + `","public_download_url":"/files/tools/report.pdf?sign=2"}`
|
|
if got := strings.TrimSpace(output.String()); got != want {
|
|
t.Fatalf("output = %s, want %s", got, want)
|
|
}
|
|
}
|
|
|
|
func TestRunFilePublicURLExplainsExpiredAuthorization(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/agent-stub/files/download-request" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
_, _ = w.Write([]byte(`{"detail":{"code":"agent_stub_authorization_expired","message":"expired"}}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
err := RunFilePublicURL(
|
|
&Environment{URL: server.URL + "/agent-stub", AuthJWE: "token"},
|
|
"dify-file-ref:eyJyZWNvcmRfaWQiOiJ0b29sLTEifQ==",
|
|
)
|
|
if err == nil {
|
|
t.Fatal("RunFilePublicURL succeeded, want expired authorization failure")
|
|
}
|
|
for _, want := range []string{
|
|
"request public download URL",
|
|
"expired after 5 minutes",
|
|
"will not refresh automatically",
|
|
"start a new shell tool call",
|
|
"retry the command",
|
|
} {
|
|
if !strings.Contains(err.Error(), want) {
|
|
t.Errorf("error = %q, want substring %q", err, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRunFileUploadRejectsNonCanonicalReferenceInBothModes(t *testing.T) {
|
|
filePath := filepath.Join(t.TempDir(), "report.pdf")
|
|
if err := os.WriteFile(filePath, []byte("report"), 0o600); err != nil {
|
|
t.Fatalf("write fixture: %v", err)
|
|
}
|
|
|
|
for _, noDownloadLink := range []bool{false, true} {
|
|
client := &fakeFileUploadClient{uploadResponse: []byte(`{"reference":"raw-id"}`)}
|
|
var output bytes.Buffer
|
|
err := runFileUpload(client, filePath, noDownloadLink, &output)
|
|
if err == nil || !strings.Contains(err.Error(), "invalid reference") {
|
|
t.Fatalf("noDownloadLink=%t error = %v, want invalid reference", noDownloadLink, err)
|
|
}
|
|
if client.downloadRequestCall != 0 || output.Len() != 0 {
|
|
t.Fatalf(
|
|
"noDownloadLink=%t download calls = %d, output = %q; want no download or output",
|
|
noDownloadLink,
|
|
client.downloadRequestCall,
|
|
output.String(),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRunFileUploadRejectsMissingReferenceInBothModes(t *testing.T) {
|
|
filePath := filepath.Join(t.TempDir(), "report.pdf")
|
|
if err := os.WriteFile(filePath, []byte("report"), 0o600); err != nil {
|
|
t.Fatalf("write fixture: %v", err)
|
|
}
|
|
|
|
for _, noDownloadLink := range []bool{false, true} {
|
|
client := &fakeFileUploadClient{uploadResponse: []byte(`{"reference":""}`)}
|
|
var output bytes.Buffer
|
|
err := runFileUpload(client, filePath, noDownloadLink, &output)
|
|
if err == nil || !strings.Contains(err.Error(), "missing reference") {
|
|
t.Fatalf("noDownloadLink=%t error = %v, want missing reference", noDownloadLink, err)
|
|
}
|
|
if client.downloadRequestCall != 0 {
|
|
t.Fatalf("noDownloadLink=%t download request calls = %d, want 0", noDownloadLink, client.downloadRequestCall)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRunFileUploadRejectsInvalidUploadResponseBeforeDownloadRequest(t *testing.T) {
|
|
filePath := filepath.Join(t.TempDir(), "report.pdf")
|
|
if err := os.WriteFile(filePath, []byte("report"), 0o600); err != nil {
|
|
t.Fatalf("write fixture: %v", err)
|
|
}
|
|
|
|
client := &fakeFileUploadClient{uploadResponse: []byte("not-json")}
|
|
var output bytes.Buffer
|
|
err := runFileUpload(client, filePath, true, &output)
|
|
if err == nil || !strings.Contains(err.Error(), "parse upload result") {
|
|
t.Fatalf("error = %v, want parse upload result failure", err)
|
|
}
|
|
if client.downloadRequestCall != 0 {
|
|
t.Fatalf("download request calls = %d, want 0", client.downloadRequestCall)
|
|
}
|
|
}
|
|
|
|
func TestRunFileDownloadRequestsSandboxURLAndWritesFile(t *testing.T) {
|
|
var requestPayload map[string]json.RawMessage
|
|
var server *httptest.Server
|
|
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/agent-stub/files/download-request":
|
|
if err := json.NewDecoder(r.Body).Decode(&requestPayload); err != nil {
|
|
t.Errorf("decode download request: %v", err)
|
|
http.Error(w, "bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"filename":"report.pdf","mime_type":"application/pdf","size":6,"download_url":"` + server.URL + `/files/report.pdf"}`))
|
|
case "/files/report.pdf":
|
|
_, _ = w.Write([]byte("report"))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
targetDir := t.TempDir()
|
|
err := RunFileDownload(
|
|
&Environment{URL: server.URL + "/agent-stub", AuthJWE: "test-token"},
|
|
"tool_file",
|
|
"dify-file-ref:canonical",
|
|
targetDir,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("run file download: %v", err)
|
|
}
|
|
|
|
var forFrontend bool
|
|
if err := json.Unmarshal(requestPayload["for_frontend"], &forFrontend); err != nil {
|
|
t.Fatalf("decode for_frontend: %v", err)
|
|
}
|
|
if forFrontend {
|
|
t.Fatal("download request selected a frontend URL")
|
|
}
|
|
data, err := os.ReadFile(filepath.Join(targetDir, "report.pdf"))
|
|
if err != nil {
|
|
t.Fatalf("read downloaded file: %v", err)
|
|
}
|
|
if string(data) != "report" {
|
|
t.Fatalf("downloaded file = %q, want report", data)
|
|
}
|
|
}
|