Files
dingtalk-workspace-cli/internal/shortcut/chat/resource_download_coverage_test.go
T

820 lines
27 KiB
Go

// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package chat
import (
"bytes"
"context"
"errors"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/helpers"
)
func resetResourceDownloadHooks(t *testing.T) {
t.Helper()
getwd := resourceGetwd
abs := resourceAbs
eval := resourceEvalSymlinks
stat := resourceStat
lstat := resourceLstat
rel := resourceRel
mkdir := resourceMkdir
createTemp := resourceCreateTemp
copyFn := resourceCopy
syncFn := resourceTempSync
closeFn := resourceTempClose
renameFn := resourceRename
linkFn := resourceLink
downloadFn := resourceDownload
t.Cleanup(func() {
resourceGetwd = getwd
resourceAbs = abs
resourceEvalSymlinks = eval
resourceStat = stat
resourceLstat = lstat
resourceRel = rel
resourceMkdir = mkdir
resourceCreateTemp = createTemp
resourceCopy = copyFn
resourceTempSync = syncFn
resourceTempClose = closeFn
resourceRename = renameFn
resourceLink = linkFn
resourceDownload = downloadFn
})
}
func TestCrossPlatformCoverageResourceDownloadCommandOutcomes(t *testing.T) {
baseArgs := []string{
"chat", "+messages-resource-download",
"--resource-id", "@image",
"--message-id", "msg",
"--open-conversation-id", "cid",
"--yes",
}
t.Run("dry run", func(t *testing.T) {
helpers.InitDeps(&larkAlignmentCaller{})
root := newPlatformCoverageRoot()
root.SetArgs(append(append([]string{}, baseArgs...), "--dry-run"))
if err := root.Execute(); err != nil {
t.Fatal(err)
}
})
t.Run("lower error", func(t *testing.T) {
helpers.InitDeps(&larkAlignmentCaller{failProductTool: "im/get_resource_download_url"})
root := newPlatformCoverageRoot()
root.SetArgs(baseArgs)
if err := root.Execute(); err == nil {
t.Fatal("lower error was swallowed")
}
})
t.Run("invalid download info", func(t *testing.T) {
helpers.InitDeps(&larkAlignmentCaller{responses: map[string]string{
"im/get_resource_download_url": `{"result":{}}`,
}})
root := newPlatformCoverageRoot()
root.SetArgs(baseArgs)
if err := root.Execute(); err == nil {
t.Fatal("missing URL was accepted")
}
})
t.Run("getwd error", func(t *testing.T) {
resetResourceDownloadHooks(t)
resourceGetwd = func() (string, error) { return "", errors.New("getwd") }
helpers.InitDeps(&larkAlignmentCaller{responses: map[string]string{
"im/get_resource_download_url": `{"result":{"resourceUrl":"https://download.dingtalk.com/file"}}`,
}})
root := newPlatformCoverageRoot()
root.SetArgs(baseArgs)
if err := root.Execute(); err == nil {
t.Fatal("getwd error was swallowed")
}
})
t.Run("path error", func(t *testing.T) {
resetResourceDownloadHooks(t)
resourceAbs = func(string) (string, error) { return "", errors.New("path") }
helpers.InitDeps(&larkAlignmentCaller{responses: map[string]string{
"im/get_resource_download_url": `{"result":{"resourceUrl":"https://download.dingtalk.com/file"}}`,
}})
root := newPlatformCoverageRoot()
root.SetArgs(baseArgs)
if err := root.Execute(); err == nil {
t.Fatal("path error was swallowed")
}
})
for _, tc := range []struct {
name string
download func(context.Context, *http.Client, string, map[string]string, string, bool) (int64, error)
wantError bool
}{
{
name: "download error",
download: func(context.Context, *http.Client, string, map[string]string, string, bool) (int64, error) {
return 0, errors.New("download")
},
wantError: true,
},
{
name: "success",
download: func(context.Context, *http.Client, string, map[string]string, string, bool) (int64, error) {
return 7, nil
},
},
} {
t.Run(tc.name, func(t *testing.T) {
resetResourceDownloadHooks(t)
resourceDownload = tc.download
helpers.InitDeps(&larkAlignmentCaller{responses: map[string]string{
"im/get_resource_download_url": `{"result":{"resourceUrl":"https://download.dingtalk.com/file"}}`,
}})
root := newPlatformCoverageRoot()
root.SetArgs(append(append([]string{}, baseArgs...), "--output", filepath.Join(t.TempDir(), "file")))
// The absolute path is intentionally rejected by the public command,
// so use a relative path while running from the repository cwd.
root.SetArgs(append(append([]string{}, baseArgs...), "--output", "coverage-resource.bin", "--overwrite"))
err := root.Execute()
if (err != nil) != tc.wantError {
t.Fatalf("error = %v, wantError=%v", err, tc.wantError)
}
})
}
}
func TestCrossPlatformCoverageResourceDownloadValidationAndInfo(t *testing.T) {
for _, output := range []string{"", "/absolute", "../escape"} {
if err := validateResourceDownloadOutput(output); err == nil {
t.Errorf("unsafe output %q accepted", output)
}
}
for _, data := range []map[string]any{
{},
{"resourceUrl": []any{42, ""}},
{"resourceUrl": "://bad"},
{"resourceUrl": "http://example.test/file"},
} {
if _, _, err := resourceDownloadInfo(data); err == nil {
t.Errorf("invalid resource info accepted: %#v", data)
}
}
resourceURL, headers, err := resourceDownloadInfo(map[string]any{
"resourceUrl": "https://download.dingtalk.com/file",
"headers": map[string]any{
"": "ignored",
"X-Count": 3,
"X-Test": "ok",
},
})
if err != nil || resourceURL == "" || len(headers) != 1 || headers["X-Test"] != "ok" {
t.Fatalf("resource info = %q %#v %v", resourceURL, headers, err)
}
for host, want := range map[string]bool{
"ALIYUNCS.COM.": false,
"bucket.aliyuncs.com": false,
"bucket.oss-cn-hangzhou.aliyuncs.com": true,
"bucket.oss-internal.aliyuncs.com": false,
"bucket.oss-cn-hangzhou-internal.aliyuncs.com": false,
"example.com": false,
} {
if got := isAliyunOSSHost(host); got != want {
t.Errorf("isAliyunOSSHost(%q) = %v, want %v", host, got, want)
}
}
for host, want := range map[string]bool{
"DINGTALK.COM.": true,
"download.dingtalk.com": true,
"bucket.oss-cn-hangzhou.aliyuncs.com": true,
"aliyuncs.com.evil.test": false,
"evildingtalk.com": false,
"download.example.invalid": false,
} {
if got := isResourceDownloadAllowedHost(host); got != want {
t.Errorf("isResourceDownloadAllowedHost(%q) = %v, want %v", host, got, want)
}
}
}
func TestCrossPlatformCoverageResourceDownloadPathErrors(t *testing.T) {
t.Run("absolute resolution", func(t *testing.T) {
resetResourceDownloadHooks(t)
resourceAbs = func(string) (string, error) { return "", errors.New("abs") }
if _, _, err := resolveResourceDownloadPath(".", "file", "https://example.test/file", false); err == nil {
t.Fatal("absolute-path error was swallowed")
}
})
t.Run("base symlink resolution", func(t *testing.T) {
resetResourceDownloadHooks(t)
resourceEvalSymlinks = func(string) (string, error) { return "", errors.New("eval") }
if _, _, err := resolveResourceDownloadPath(t.TempDir(), "file", "https://example.test/file", false); err == nil {
t.Fatal("base eval error was swallowed")
}
})
t.Run("parent symlink resolution", func(t *testing.T) {
resetResourceDownloadHooks(t)
calls := 0
resourceEvalSymlinks = func(path string) (string, error) {
calls++
if calls == 2 {
return "", errors.New("parent eval")
}
return filepath.EvalSymlinks(path)
}
if _, _, err := resolveResourceDownloadPath(t.TempDir(), "file", "https://example.test/file", false); err == nil {
t.Fatal("parent eval error was swallowed")
}
})
t.Run("parent escape", func(t *testing.T) {
resetResourceDownloadHooks(t)
calls := 0
resourceRel = func(base, target string) (string, error) {
calls++
if calls == 2 {
return "../outside", nil
}
return filepath.Rel(base, target)
}
if _, _, err := resolveResourceDownloadPath(t.TempDir(), "file", "https://example.test/file", false); err == nil {
t.Fatal("parent escape was accepted")
}
})
t.Run("target inspection", func(t *testing.T) {
base := t.TempDir()
existing := filepath.Join(base, "existing")
if err := os.WriteFile(existing, []byte("x"), 0o600); err != nil {
t.Fatal(err)
}
if _, _, err := resolveResourceDownloadPath(base, "existing", "https://example.test/file", false); err == nil {
t.Fatal("existing file was accepted without overwrite")
}
if _, _, err := resolveResourceDownloadPath(base, "existing", "https://example.test/file", true); err != nil {
t.Fatal(err)
}
dir := filepath.Join(base, "dir")
if err := os.Mkdir(dir, 0o755); err != nil {
t.Fatal(err)
}
if _, _, err := resolveResourceDownloadPath(base, filepath.Join("dir", "child")+"/", "https://example.test/file", false); err != nil {
t.Fatal(err)
}
link := filepath.Join(base, "link")
if err := os.Symlink(existing, link); err == nil {
if _, _, resolveErr := resolveResourceDownloadPath(base, "link", "https://example.test/file", false); resolveErr == nil {
t.Fatal("symlink target was accepted")
}
}
})
t.Run("lstat error", func(t *testing.T) {
resetResourceDownloadHooks(t)
resourceLstat = func(string) (os.FileInfo, error) { return nil, errors.New("lstat") }
if _, _, err := resolveResourceDownloadPath(t.TempDir(), "file", "https://example.test/file", false); err == nil {
t.Fatal("lstat error was swallowed")
}
})
t.Run("target becomes directory", func(t *testing.T) {
resetResourceDownloadHooks(t)
base := t.TempDir()
resourceStat = func(string) (os.FileInfo, error) { return nil, os.ErrNotExist }
resourceLstat = func(string) (os.FileInfo, error) {
return os.Stat(base)
}
if _, _, err := resolveResourceDownloadPath(base, "file", "https://example.test/file", false); err == nil {
t.Fatal("directory target was accepted")
}
})
t.Run("final relative error", func(t *testing.T) {
resetResourceDownloadHooks(t)
calls := 0
resourceRel = func(base, target string) (string, error) {
calls++
if calls == 3 {
return "", errors.New("rel")
}
return filepath.Rel(base, target)
}
if _, _, err := resolveResourceDownloadPath(t.TempDir(), "file", "https://example.test/file", false); err == nil {
t.Fatal("final relative error was swallowed")
}
})
}
func TestCrossPlatformCoverageEnsureResourceDownloadParentErrors(t *testing.T) {
base := t.TempDir()
if err := ensureResourceDownloadParent(base, filepath.Join(base, "..", "outside")); err == nil {
t.Fatal("outside parent was accepted")
}
if err := ensureResourceDownloadParent(base, base); err != nil {
t.Fatal(err)
}
t.Run("mkdir and recheck error", func(t *testing.T) {
resetResourceDownloadHooks(t)
resourceMkdir = func(string, os.FileMode) error { return errors.New("mkdir") }
resourceLstat = func(string) (os.FileInfo, error) { return nil, os.ErrNotExist }
if err := ensureResourceDownloadParent(base, filepath.Join(base, "new")); err == nil {
t.Fatal("mkdir error was swallowed")
}
})
t.Run("concurrent creator", func(t *testing.T) {
resetResourceDownloadHooks(t)
target := filepath.Join(base, "concurrent")
resourceMkdir = func(path string, mode os.FileMode) error {
if err := os.Mkdir(path, mode); err != nil {
t.Fatal(err)
}
return errors.New("lost race")
}
if err := ensureResourceDownloadParent(base, target); err != nil {
t.Fatal(err)
}
})
t.Run("inspection error", func(t *testing.T) {
resetResourceDownloadHooks(t)
resourceLstat = func(string) (os.FileInfo, error) { return nil, errors.New("inspect") }
if err := ensureResourceDownloadParent(base, filepath.Join(base, "inspect")); err == nil {
t.Fatal("inspection error was swallowed")
}
})
file := filepath.Join(base, "file")
if err := os.WriteFile(file, []byte("x"), 0o600); err != nil {
t.Fatal(err)
}
if err := ensureResourceDownloadParent(base, file); err == nil {
t.Fatal("file parent was accepted")
}
link := filepath.Join(base, "link-parent")
if err := os.Symlink(base, link); err == nil {
if err := ensureResourceDownloadParent(base, link); err == nil {
t.Fatal("symlink parent was accepted")
}
}
}
func TestCrossPlatformCoverageResourceDownloadFilenameFallbacks(t *testing.T) {
for _, resourceURL := range []string{"://bad", "https://example.test/", "https://example.test/%zz"} {
if got := resourceDownloadFilename(resourceURL); got != "download" {
t.Errorf("resourceDownloadFilename(%q) = %q", resourceURL, got)
}
}
if got := resourceDownloadFilename("https://example.test/a%20b.txt"); got != "a b.txt" {
t.Fatalf("decoded filename = %q", got)
}
for _, unsafeName := range []string{
"..",
"CON",
"nul.txt",
"COM1.log",
"trailing.",
"trailing ",
"line\nbreak.txt",
`bad:name.txt`,
} {
got := resourceDownloadFilename(
"https://download.dingtalk.com/fallback.bin",
unsafeName,
)
if got != "fallback.bin" {
t.Errorf("unsafe preferred name %q produced %q", unsafeName, got)
}
}
}
type resourceRoundTripper func(*http.Request) (*http.Response, error)
func (f resourceRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
return f(request)
}
func resourceResponseClient(status int, body string, length int64) *http.Client {
return &http.Client{Transport: resourceRoundTripper(func(*http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: status,
Body: io.NopCloser(strings.NewReader(body)),
ContentLength: length,
Header: make(http.Header),
}, nil
})}
}
func TestCrossPlatformCoverageDownloadResourceHTTPFailures(t *testing.T) {
dest := filepath.Join(t.TempDir(), "resource")
if _, err := downloadResourceAtomically(context.Background(), nil, ":", nil, dest, false); err == nil {
t.Fatal("invalid request URL was accepted")
}
errorClient := &http.Client{Transport: resourceRoundTripper(func(*http.Request) (*http.Response, error) {
return nil, errors.New("transport")
})}
if _, err := downloadResourceAtomically(context.Background(), errorClient, "https://download.dingtalk.com/file", nil, dest, false); err == nil {
t.Fatal("transport error was swallowed")
}
if _, err := downloadResourceAtomically(context.Background(), resourceResponseClient(500, "", 0), "https://download.dingtalk.com/file", nil, dest, false); err == nil {
t.Fatal("HTTP 500 was accepted")
}
if _, err := downloadResourceAtomically(context.Background(), resourceResponseClient(200, "x", 2), "https://download.dingtalk.com/file", nil, dest, false); err == nil {
t.Fatal("content-length mismatch was accepted")
}
nilClientDest := filepath.Join(t.TempDir(), "nil-client")
if _, err := downloadResourceAtomically(
context.Background(), nil, "https://evil.example/file",
map[string]string{"X-Test": "ok"}, nilClientDest, true,
); err == nil {
t.Fatal("nil-client path accepted an untrusted URL")
}
}
func TestCrossPlatformCoverageDownloadResourceRedirectGuards(t *testing.T) {
httpRedirect := &http.Client{Transport: resourceRoundTripper(func(*http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusFound,
Body: io.NopCloser(strings.NewReader("")),
Header: http.Header{"Location": []string{"http://evil.example/file"}},
}, nil
})}
if _, err := downloadResourceAtomically(
context.Background(), httpRedirect, "https://download.dingtalk.com/start", nil,
filepath.Join(t.TempDir(), "http-redirect"), false,
); err == nil {
t.Fatal("HTTP redirect was accepted")
}
loop := &http.Client{Transport: resourceRoundTripper(func(*http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusFound,
Body: io.NopCloser(strings.NewReader("")),
Header: http.Header{"Location": []string{"https://download.dingtalk.com/loop"}},
}, nil
})}
if _, err := downloadResourceAtomically(
context.Background(), loop, "https://download.dingtalk.com/loop", nil,
filepath.Join(t.TempDir(), "loop"), false,
); err == nil {
t.Fatal("redirect loop was accepted")
}
original := *loop
original.CheckRedirect = func(*http.Request, []*http.Request) error {
return errors.New("original redirect policy")
}
if _, err := downloadResourceAtomically(
context.Background(), &original, "https://download.dingtalk.com/loop", nil,
filepath.Join(t.TempDir(), "original"), false,
); err == nil {
t.Fatal("original redirect rejection was swallowed")
}
redirectCount := 0
crossHost := &http.Client{Transport: resourceRoundTripper(func(request *http.Request) (*http.Response, error) {
redirectCount++
switch redirectCount {
case 1:
if request.Header.Get("X-Resource-Token") != "secret" {
t.Fatal("initial signed header missing")
}
return &http.Response{
StatusCode: http.StatusFound,
Body: io.NopCloser(strings.NewReader("")),
Header: http.Header{"Location": []string{"https://bucket.oss-cn-hangzhou.aliyuncs.com/intermediate"}},
}, nil
case 2:
if request.Header.Get("X-Resource-Token") != "" {
t.Fatal("server-supplied header leaked on first cross-host hop")
}
return &http.Response{
StatusCode: http.StatusFound,
Body: io.NopCloser(strings.NewReader("")),
Header: http.Header{"Location": []string{"https://bucket.oss-cn-hangzhou.aliyuncs.com/final"}},
}, nil
default:
if request.Header.Get("X-Resource-Token") != "" {
t.Fatal("server-supplied header was restored on later same-host hop")
}
}
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader("ok")),
ContentLength: 2,
Header: make(http.Header),
}, nil
})}
if _, err := downloadResourceAtomically(
context.Background(), crossHost, "https://download.dingtalk.com/start",
map[string]string{"X-Resource-Token": "secret"},
filepath.Join(t.TempDir(), "cross-host"), false,
); err != nil {
t.Fatal(err)
}
}
func TestCrossPlatformCoverageDownloadResourceFileFailures(t *testing.T) {
client := resourceResponseClient(http.StatusOK, "body", 4)
run := func(t *testing.T, overwrite bool) error {
t.Helper()
_, err := downloadResourceAtomically(
context.Background(), client, "https://download.dingtalk.com/file", nil,
filepath.Join(t.TempDir(), "resource"), overwrite)
return err
}
t.Run("create temp", func(t *testing.T) {
resetResourceDownloadHooks(t)
resourceCreateTemp = func(string, string) (*os.File, error) { return nil, errors.New("create") }
if err := run(t, false); err == nil {
t.Fatal("create-temp error was swallowed")
}
})
t.Run("copy", func(t *testing.T) {
resetResourceDownloadHooks(t)
resourceCopy = func(io.Writer, io.Reader) (int64, error) { return 0, errors.New("copy") }
if err := run(t, false); err == nil {
t.Fatal("copy error was swallowed")
}
})
t.Run("sync", func(t *testing.T) {
resetResourceDownloadHooks(t)
resourceTempSync = func(*os.File) error { return errors.New("sync") }
if err := run(t, false); err == nil {
t.Fatal("sync error was swallowed")
}
})
t.Run("close", func(t *testing.T) {
resetResourceDownloadHooks(t)
calls := 0
resourceTempClose = func(file *os.File) error {
calls++
if calls == 1 {
return errors.New("close")
}
return file.Close()
}
if err := run(t, false); err == nil {
t.Fatal("close error was swallowed")
}
})
t.Run("rename", func(t *testing.T) {
resetResourceDownloadHooks(t)
resourceRename = func(string, string) error { return errors.New("rename") }
if err := run(t, true); err == nil {
t.Fatal("rename error was swallowed")
}
})
t.Run("link exists", func(t *testing.T) {
resetResourceDownloadHooks(t)
resourceLink = func(string, string) error { return os.ErrExist }
if err := run(t, false); err == nil {
t.Fatal("link-exists error was swallowed")
}
})
t.Run("link other", func(t *testing.T) {
resetResourceDownloadHooks(t)
resourceLink = func(string, string) error { return errors.New("link") }
if err := run(t, false); err == nil {
t.Fatal("link error was swallowed")
}
})
}
func TestCrossPlatformCoverageDownloadResourceCopySuccessWithBuffer(t *testing.T) {
resetResourceDownloadHooks(t)
var copied bytes.Buffer
resourceCopy = func(writer io.Writer, reader io.Reader) (int64, error) {
body, err := io.ReadAll(reader)
if err != nil {
return 0, err
}
copied.Write(body)
written, writeErr := writer.Write(body)
return int64(written), writeErr
}
dest := filepath.Join(t.TempDir(), "resource")
if _, err := downloadResourceAtomically(
context.Background(), resourceResponseClient(200, "ok", 2),
"https://download.dingtalk.com/file", nil, dest, true,
); err != nil {
t.Fatal(err)
}
if copied.String() != "ok" {
t.Fatalf("copied = %q", copied.String())
}
}
func TestCrossPlatformCoverageMessageExportFailureBoundaries(t *testing.T) {
for _, output := range []string{"exports/", "exports/messages.txt"} {
if err := ValidateMessageExportOutput(output); err == nil {
t.Fatalf("ValidateMessageExportOutput(%q) succeeded", output)
}
}
if _, _, err := WriteMessageExportJSON("not-json.txt", false, map[string]any{}); err == nil {
t.Fatal("invalid export path reached write pipeline")
}
baseSetup := func(t *testing.T) string {
t.Helper()
resetResourceDownloadHooks(t)
base := t.TempDir()
resourceGetwd = func() (string, error) { return base, nil }
resourceAbs = filepath.Abs
resourceEvalSymlinks = filepath.EvalSymlinks
resourceLstat = os.Lstat
resourceRel = filepath.Rel
resourceCreateTemp = os.CreateTemp
resourceTempSync = (*os.File).Sync
resourceTempClose = (*os.File).Close
resourceRename = os.Rename
resourceLink = os.Link
return base
}
t.Run("path setup", func(t *testing.T) {
baseSetup(t)
resourceGetwd = func() (string, error) { return "", errors.New("getwd") }
if _, _, err := WriteMessageExportJSON("out.json", false, map[string]any{}); err == nil {
t.Fatal("getwd failure ignored")
}
})
t.Run("absolute path", func(t *testing.T) {
baseSetup(t)
resourceAbs = func(string) (string, error) { return "", errors.New("abs") }
if _, _, err := WriteMessageExportJSON("out.json", false, map[string]any{}); err == nil {
t.Fatal("abs failure ignored")
}
})
t.Run("base symlink", func(t *testing.T) {
baseSetup(t)
resourceEvalSymlinks = func(string) (string, error) { return "", errors.New("eval") }
if _, _, err := WriteMessageExportJSON("out.json", false, map[string]any{}); err == nil {
t.Fatal("base symlink failure ignored")
}
})
t.Run("parent symlink", func(t *testing.T) {
base := baseSetup(t)
calls := 0
resourceEvalSymlinks = func(path string) (string, error) {
calls++
if calls == 1 {
return base, nil
}
return "", errors.New("parent")
}
if _, _, err := WriteMessageExportJSON("exports/out.json", false, map[string]any{}); err == nil {
t.Fatal("parent symlink failure ignored")
}
})
t.Run("parent creation", func(t *testing.T) {
baseSetup(t)
resourceLstat = func(string) (os.FileInfo, error) {
return os.Stat(filepath.Join(t.TempDir(), "missing-parent"))
}
resourceMkdir = func(string, os.FileMode) error { return errors.New("mkdir") }
if _, _, err := WriteMessageExportJSON("exports/out.json", false, map[string]any{}); err == nil {
t.Fatal("parent creation failure ignored")
}
})
t.Run("parent escapes", func(t *testing.T) {
base := baseSetup(t)
resourceEvalSymlinks = func(path string) (string, error) {
if path == base {
return base, nil
}
return filepath.Dir(base), nil
}
if _, _, err := WriteMessageExportJSON("exports/out.json", false, map[string]any{}); err == nil {
t.Fatal("parent escape accepted")
}
})
for _, tc := range []struct {
name string
setup func(t *testing.T, base string)
}{
{name: "existing symlink", setup: func(t *testing.T, base string) {
target := filepath.Join(base, "out.json")
if err := os.Symlink("missing", target); err != nil {
t.Fatal(err)
}
}},
{name: "existing directory", setup: func(t *testing.T, base string) {
if err := os.Mkdir(filepath.Join(base, "out.json"), 0o755); err != nil {
t.Fatal(err)
}
}},
{name: "existing no clobber", setup: func(t *testing.T, base string) {
if err := os.WriteFile(filepath.Join(base, "out.json"), []byte("old"), 0o600); err != nil {
t.Fatal(err)
}
}},
{name: "lstat error", setup: func(t *testing.T, _ string) {
resourceLstat = func(string) (os.FileInfo, error) { return nil, errors.New("lstat") }
}},
} {
t.Run(tc.name, func(t *testing.T) {
base := baseSetup(t)
tc.setup(t, base)
if _, _, err := WriteMessageExportJSON("out.json", false, map[string]any{}); err == nil {
t.Fatal("invalid existing target accepted")
}
})
}
for _, tc := range []struct {
name string
overwrite bool
payload any
setup func(t *testing.T, base string)
}{
{name: "marshal", payload: make(chan int)},
{name: "create temp", payload: map[string]any{}, setup: func(t *testing.T, _ string) {
resourceCreateTemp = func(string, string) (*os.File, error) { return nil, errors.New("create") }
}},
{name: "write", payload: map[string]any{}, setup: func(t *testing.T, base string) {
resourceCreateTemp = func(string, string) (*os.File, error) {
file, err := os.CreateTemp(base, "closed-*")
if err == nil {
_ = file.Close()
}
return file, err
}
}},
{name: "sync", payload: map[string]any{}, setup: func(t *testing.T, _ string) {
resourceTempSync = func(*os.File) error { return errors.New("sync") }
}},
{name: "close", payload: map[string]any{}, setup: func(t *testing.T, _ string) {
resourceTempClose = func(file *os.File) error {
_ = file.Close()
return errors.New("close")
}
}},
{name: "rename", overwrite: true, payload: map[string]any{}, setup: func(t *testing.T, base string) {
if err := os.WriteFile(filepath.Join(base, "out.json"), []byte("old"), 0o600); err != nil {
t.Fatal(err)
}
resourceRename = func(string, string) error { return errors.New("rename") }
}},
{name: "link exists", payload: map[string]any{}, setup: func(t *testing.T, _ string) {
resourceLink = func(string, string) error { return os.ErrExist }
}},
{name: "link", payload: map[string]any{}, setup: func(t *testing.T, _ string) {
resourceLink = func(string, string) error { return errors.New("link") }
}},
{name: "final rel", overwrite: true, payload: map[string]any{}, setup: func(t *testing.T, base string) {
if err := os.WriteFile(filepath.Join(base, "out.json"), []byte("old"), 0o600); err != nil {
t.Fatal(err)
}
calls := 0
resourceRel = func(from, to string) (string, error) {
calls++
if calls < 3 {
return filepath.Rel(from, to)
}
return "", errors.New("rel")
}
}},
} {
t.Run(tc.name, func(t *testing.T) {
base := baseSetup(t)
if tc.setup != nil {
tc.setup(t, base)
}
if _, _, err := WriteMessageExportJSON("out.json", tc.overwrite, tc.payload); err == nil {
t.Fatal("failure hook was ignored")
}
})
}
}
func TestCrossPlatformCoverageMessageExportOverwriteReplacesExistingFile(t *testing.T) {
resetResourceDownloadHooks(t)
base := t.TempDir()
resourceGetwd = func() (string, error) { return base, nil }
resourceAbs = filepath.Abs
resourceEvalSymlinks = filepath.EvalSymlinks
resourceLstat = os.Lstat
resourceRel = filepath.Rel
resourceCreateTemp = os.CreateTemp
resourceTempSync = (*os.File).Sync
resourceTempClose = (*os.File).Close
resourceRename = replaceFileAtomically
resourceLink = os.Link
target := filepath.Join(base, "out.json")
if err := os.WriteFile(target, []byte("old\n"), 0o600); err != nil {
t.Fatal(err)
}
relative, size, err := WriteMessageExportJSON(
"out.json", true, map[string]any{"value": "new"},
)
if err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(target)
if err != nil {
t.Fatal(err)
}
const want = "{\n \"value\": \"new\"\n}\n"
if relative != "out.json" || size != len(want) || string(data) != want {
t.Fatalf("relative=%q size=%d data=%q", relative, size, data)
}
}