增加复制粘贴交互

This commit is contained in:
andy committed 2026-09-30 13:22:53 +08:00
1 parent 4195525dc7
commit deb9cc5bcf
14 files changed
+1144 -31

No files matched your search

+98
View File
@@ -4,6 +4,9 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"image"
"image/png"
"io"
"mime/multipart"
"net/http"
@@ -14,6 +17,7 @@ import (
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/assets"
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/httpapi"
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/identity"
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/publicapi"
)
@@ -204,6 +208,100 @@ func TestAssetsLimitsAndInfrastructureErrorsDoNotLeak(t *testing.T) {
}
}
type imageImportRemote struct {
data []byte
err error
called bool
}
func (f *imageImportRemote) Fetch(context.Context, string) (assets.Blob, error) {
f.called = true
if f.err != nil {
return assets.Blob{}, f.err
}
return assets.Blob{Body: io.NopCloser(bytes.NewReader(f.data)), ContentType: "text/html", Size: int64(len(f.data))}, nil
}
type imageImportResolver struct{}
func (imageImportResolver) Resolve(context.Context, string) (identity.Session, error) {
return identity.Session{User: identity.User{ID: "owner-a", Role: "user"}}, nil
}
func TestImportImageHTTPAuthMethodAndStoredResult(t *testing.T) {
var encoded bytes.Buffer
if err := png.Encode(&encoded, image.NewRGBA(image.Rect(0, 0, 2, 2))); err != nil {
t.Fatal(err)
}
remote := &imageImportRemote{data: encoded.Bytes()}
cat, blobs := &assetCatalog{}, &assetBlobs{values: map[string][]byte{}}
svc := assets.NewService(cat, blobs, remote, time.Now, func(prefix string) string { return prefix + "-1" })
platform, _ := httpapi.NewPlatformAuthorizer(httpapi.AuthState{Required: true, Configured: true}, imageImportResolver{})
h, _ := httpapi.NewAssetsHandler(svc, platform, publicapi.NewAuthenticator(publicapi.Config{APIKeys: "agent:secret"}), httpapi.AssetsConfig{})
path := "/api/assets/import-image"
unauth := request(t, h, http.MethodPost, path, strings.NewReader(`{"url":"https://image.test/a.png"}`), map[string]string{"Content-Type": "application/json", "Authorization": "Bearer secret"})
if unauth.Code != 401 || remote.called {
t.Fatalf("unauth=%d called=%v", unauth.Code, remote.called)
}
wrongMethod := request(t, h, http.MethodGet, path, nil, nil)
if wrongMethod.Code != 405 || wrongMethod.Header().Get("Allow") != "POST, OPTIONS" {
t.Fatalf("method=%d allow=%q", wrongMethod.Code, wrongMethod.Header().Get("Allow"))
}
options := request(t, h, http.MethodOptions, path, nil, nil)
if options.Code != 204 || options.Header().Get("Allow") != "POST, OPTIONS" {
t.Fatalf("options=%d allow=%q", options.Code, options.Header().Get("Allow"))
}
for _, headers := range []map[string]string{
{"Cookie": identity.SessionCookieName + "=valid", "Content-Type": "text/plain"},
{"Cookie": identity.SessionCookieName + "=valid", "Content-Type": "application/json", "Sec-Fetch-Site": "cross-site"},
} {
bad := request(t, h, http.MethodPost, path, strings.NewReader(`{"url":"https://image.test/a.png"}`), headers)
if bad.Code != 415 && bad.Code != 403 {
t.Fatalf("unsafe request=%d %s", bad.Code, bad.Body.String())
}
}
good := request(t, h, http.MethodPost, path, strings.NewReader(`{"url":"https://image.test/a.svg?Signature=sensitive"}`), map[string]string{"Cookie": identity.SessionCookieName + "=valid", "Content-Type": "application/json", "Sec-Fetch-Site": "same-origin"})
if good.Code != 201 {
t.Fatalf("import=%d %s", good.Code, good.Body.String())
}
var response struct {
Asset assets.Asset `json:"asset"`
}
if err := json.Unmarshal(good.Body.Bytes(), &response); err != nil {
t.Fatal(err)
}
if response.Asset.OwnerID != "owner-a" || response.Asset.Source != assets.SourceUpload || response.Asset.Kind != assets.KindImage || response.Asset.Metadata["importedFrom"] != nil || !bytes.Equal(blobs.values[response.Asset.StoragePath], encoded.Bytes()) || strings.Contains(good.Body.String(), "sensitive") {
t.Fatalf("response=%s", good.Body.String())
}
}
func TestImportImageHTTPErrorsNeverLeakRemoteURLOrCreateRecord(t *testing.T) {
secretURL := "https://image.test/private.png?Signature=secret-token"
for _, tc := range []struct {
name string
data []byte
err error
status int
message string
}{
{"invalid format", []byte("<html>access denied</html>"), nil, 415, "不支持的图片格式"},
{"unavailable", nil, errors.New("GET " + secretURL + ": timeout"), 502, "图片地址不可访问"},
{"too large", nil, assets.ErrRemoteTooLarge, 413, "图片过大"},
} {
t.Run(tc.name, func(t *testing.T) {
cat, blobs := &assetCatalog{}, &assetBlobs{values: map[string][]byte{}}
remote := &imageImportRemote{data: tc.data, err: tc.err}
svc := assets.NewService(cat, blobs, remote, time.Now, nil)
platform, _ := httpapi.NewPlatformAuthorizer(httpapi.AuthState{}, nil)
h, _ := httpapi.NewAssetsHandler(svc, platform, publicapi.NewAuthenticator(publicapi.Config{}), httpapi.AssetsConfig{})
resp := request(t, h, http.MethodPost, "/api/assets/import-image", strings.NewReader(`{"url":"`+secretURL+`"}`), map[string]string{"Content-Type": "application/json"})
if resp.Code != tc.status || !strings.Contains(resp.Body.String(), tc.message) || strings.Contains(resp.Body.String(), "secret-token") || len(cat.values) != 0 || len(blobs.values) != 0 {
t.Fatalf("status=%d body=%s records=%d blobs=%d", resp.Code, resp.Body.String(), len(cat.values), len(blobs.values))
}
})
}
}
func request(t *testing.T, h http.Handler, method, path string, body io.Reader, headers map[string]string) *httptest.ResponseRecorder {
t.Helper()
r := httptest.NewRequest(method, path, body)