// 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) } }