206 lines
6.5 KiB
Go
206 lines
6.5 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package chat
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestCrossPlatformCoverageCanonicalMessageResourceType(t *testing.T) {
|
|
for input, want := range map[string]string{
|
|
"mediaId": "mediaId",
|
|
"MEDIAID": "mediaId",
|
|
" fileid ": "fileId",
|
|
"FileId": "fileId",
|
|
} {
|
|
got, ok := canonicalMessageResourceType(input)
|
|
if !ok || got != want {
|
|
t.Errorf("canonicalMessageResourceType(%q) = %q, %v; want %q, true",
|
|
input, got, ok, want)
|
|
}
|
|
}
|
|
if got, ok := canonicalMessageResourceType("attachment"); ok || got != "attachment" {
|
|
t.Errorf("unsupported resource type = %q, %v", got, ok)
|
|
}
|
|
if _, err := resolveMessageResourceDownloadData(
|
|
nil, "attachment", "resource", "message", "conversation",
|
|
); err == nil || !strings.Contains(err.Error(), "不支持的消息资源类型") {
|
|
t.Fatalf("unsupported resolver type error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageResourceDownloadInfo(t *testing.T) {
|
|
url, headers, err := resourceDownloadInfo(map[string]any{
|
|
"result": map[string]any{
|
|
"resourceUrl": []any{"https://download.dingtalk.com/path/image.png"},
|
|
"headers": map[string]any{
|
|
"X-Test": "value",
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if url != "https://download.dingtalk.com/path/image.png" {
|
|
t.Fatalf("url = %q", url)
|
|
}
|
|
if headers["X-Test"] != "value" {
|
|
t.Fatalf("headers = %#v", headers)
|
|
}
|
|
upgraded, _, err := resourceDownloadInfo(map[string]any{
|
|
"result": map[string]any{
|
|
"downloadUrl": "http://bucket.oss-cn-hangzhou.aliyuncs.com/file?signature=x",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if upgraded != "https://bucket.oss-cn-hangzhou.aliyuncs.com/file?signature=x" {
|
|
t.Fatalf("upgraded URL = %q", upgraded)
|
|
}
|
|
if _, _, err := resourceDownloadInfo(map[string]any{
|
|
"result": map[string]any{"resourceUrl": "http://download.example.test/a"},
|
|
}); err == nil {
|
|
t.Fatal("plain HTTP URL unexpectedly accepted")
|
|
}
|
|
for _, resourceURL := range []string{
|
|
"https://evil.example/file",
|
|
"https://aliyuncs.com.evil.example/file",
|
|
"https://127.0.0.1/file",
|
|
"https://user:secret@download.dingtalk.com/file",
|
|
"https://download.dingtalk.com:8443/file",
|
|
} {
|
|
if _, _, err := resourceDownloadInfo(
|
|
map[string]any{"resourceUrl": resourceURL},
|
|
); err == nil {
|
|
t.Fatalf("untrusted URL %q unexpectedly accepted", resourceURL)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageResolveResourceDownloadPath(t *testing.T) {
|
|
base := t.TempDir()
|
|
if err := os.Mkdir(filepath.Join(base, "downloads"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
absolute, relative, err := resolveResourceDownloadPath(
|
|
base,
|
|
"downloads/",
|
|
"https://download.example.test/path/photo.png?token=redacted",
|
|
false,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if filepath.Base(absolute) != "photo.png" || filepath.ToSlash(relative) != "downloads/photo.png" {
|
|
t.Fatalf("path = %q / %q", absolute, relative)
|
|
}
|
|
|
|
for _, unsafe := range []string{"../escape", filepath.Join("a", "..", "..", "escape")} {
|
|
if _, _, err := resolveResourceDownloadPath(
|
|
base, unsafe, "https://download.example.test/file", false,
|
|
); err == nil {
|
|
t.Fatalf("unsafe output %q unexpectedly accepted", unsafe)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageValidateResourceDownloadOutputUsesOwningFlagName(t *testing.T) {
|
|
err := validateResourceDownloadOutputFlag("../escape", "--output-dir")
|
|
if err == nil || !strings.Contains(err.Error(), "--output-dir") {
|
|
t.Fatalf("error = %v, want --output-dir", err)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageValidateResourceDownloadOutputRejectsPortableAbsolutePaths(t *testing.T) {
|
|
for _, output := range []string{
|
|
"/absolute", `\\absolute`, `C:\\absolute`, "C:/absolute",
|
|
"C:relative", "c:relative",
|
|
} {
|
|
if err := validateResourceDownloadOutput(output); err == nil {
|
|
t.Errorf("portable absolute output %q unexpectedly accepted", output)
|
|
}
|
|
}
|
|
for _, output := range []string{"../escape", `..\\escape`} {
|
|
if err := validateResourceDownloadOutput(output); err == nil {
|
|
t.Errorf("portable parent escape %q unexpectedly accepted", output)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageResolveResourceDownloadPathRejectsSymlinkParentBeforeCreatingOutside(t *testing.T) {
|
|
base := t.TempDir()
|
|
outside := t.TempDir()
|
|
link := filepath.Join(base, "linked")
|
|
if err := os.Symlink(outside, link); err != nil {
|
|
t.Skipf("symlink unsupported: %v", err)
|
|
}
|
|
|
|
_, _, err := resolveResourceDownloadPath(
|
|
base,
|
|
filepath.Join("linked", "new-child", "resource.bin"),
|
|
"https://download.example.test/resource.bin",
|
|
false,
|
|
)
|
|
if err == nil || !strings.Contains(err.Error(), "符号链接") {
|
|
t.Fatalf("symlink-parent error = %v", err)
|
|
}
|
|
if _, statErr := os.Stat(filepath.Join(outside, "new-child")); !os.IsNotExist(statErr) {
|
|
t.Fatalf("outside directory was created before rejection: %v", statErr)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageDownloadResourceAtomically(t *testing.T) {
|
|
const body = "verified download bytes"
|
|
client := &http.Client{Transport: resourceRoundTripper(func(request *http.Request) (*http.Response, error) {
|
|
if request.URL.String() != "https://download.dingtalk.com/resource.bin" {
|
|
t.Fatalf("request URL = %q", request.URL)
|
|
}
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Body: io.NopCloser(strings.NewReader(body)),
|
|
ContentLength: int64(len(body)),
|
|
Header: make(http.Header),
|
|
}, nil
|
|
})}
|
|
dest := filepath.Join(t.TempDir(), "resource.bin")
|
|
|
|
size, err := downloadResourceAtomically(
|
|
context.Background(), client, "https://download.dingtalk.com/resource.bin", nil, dest, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if size != int64(len(body)) {
|
|
t.Fatalf("size = %d, want %d", size, len(body))
|
|
}
|
|
got, err := os.ReadFile(dest)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(got) != body {
|
|
t.Fatalf("body = %q", got)
|
|
}
|
|
if _, err := downloadResourceAtomically(
|
|
context.Background(), client, "https://download.dingtalk.com/resource.bin", nil, dest, false,
|
|
); err == nil || !strings.Contains(err.Error(), "已存在") {
|
|
t.Fatalf("no-clobber error = %v", err)
|
|
}
|
|
}
|