bf9395e022
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
194 lines
5.6 KiB
Go
194 lines
5.6 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package apps
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"compress/gzip"
|
|
"errors"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/larksuite/cli/extension/fileio"
|
|
)
|
|
|
|
// readFailingFIO opens a File whose Read always returns the configured error,
|
|
// letting tests exercise the io.Copy failure branch without filesystem games.
|
|
type readFailingFIO struct{ readErr error }
|
|
|
|
func (f readFailingFIO) Open(string) (fileio.File, error) {
|
|
return &readFailingFile{err: f.readErr}, nil
|
|
}
|
|
func (f readFailingFIO) Stat(string) (fileio.FileInfo, error) {
|
|
return nil, errors.New("Stat not used")
|
|
}
|
|
func (readFailingFIO) ResolvePath(p string) (string, error) { return p, nil }
|
|
func (readFailingFIO) Save(string, fileio.SaveOptions, io.Reader) (fileio.SaveResult, error) {
|
|
return nil, errors.New("Save not used")
|
|
}
|
|
|
|
type readFailingFile struct{ err error }
|
|
|
|
func (f *readFailingFile) Read([]byte) (int, error) { return 0, f.err }
|
|
func (f *readFailingFile) ReadAt([]byte, int64) (int, error) { return 0, f.err }
|
|
func (f *readFailingFile) Close() error { return nil }
|
|
|
|
func TestBuildHTMLPublishTarball_RoundTrip(t *testing.T) {
|
|
dir := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(dir, "index.html"), []byte("<html></html>"), 0o644); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
|
|
fio := newTestFIO()
|
|
candidates, err := walkHTMLPublishCandidates(fio, dir)
|
|
if err != nil {
|
|
t.Fatalf("walk: %v", err)
|
|
}
|
|
tarball, err := buildHTMLPublishTarball(fio, candidates)
|
|
if err != nil {
|
|
t.Fatalf("build: %v", err)
|
|
}
|
|
|
|
if len(tarball.SHA256) != 64 {
|
|
t.Fatalf("SHA256 wrong len: %d", len(tarball.SHA256))
|
|
}
|
|
if tarball.Size <= 0 || int64(len(tarball.Body)) != tarball.Size {
|
|
t.Fatalf("size=%d body=%d", tarball.Size, len(tarball.Body))
|
|
}
|
|
|
|
gz, err := gzip.NewReader(bytes.NewReader(tarball.Body))
|
|
if err != nil {
|
|
t.Fatalf("gzip: %v", err)
|
|
}
|
|
tr := tar.NewReader(gz)
|
|
hdr, err := tr.Next()
|
|
if err != nil {
|
|
t.Fatalf("tar.Next: %v", err)
|
|
}
|
|
if hdr.Name != "index.html" {
|
|
t.Fatalf("entry name = %q, want index.html", hdr.Name)
|
|
}
|
|
body, err := io.ReadAll(tr)
|
|
if err != nil || string(body) != "<html></html>" {
|
|
t.Fatalf("body=%q err=%v", body, err)
|
|
}
|
|
}
|
|
|
|
func TestBuildHTMLPublishTarball_EmptyCandidates(t *testing.T) {
|
|
if _, err := buildHTMLPublishTarball(newTestFIO(), nil); err == nil {
|
|
t.Fatalf("expected error")
|
|
}
|
|
}
|
|
|
|
func TestWriteHTMLPublishTarEntry_OpenFailure(t *testing.T) {
|
|
// candidate 指向不存在文件 → fio.Open 失败 → 错误返回
|
|
tw := tar.NewWriter(io.Discard)
|
|
defer tw.Close()
|
|
err := writeHTMLPublishTarEntry(newTestFIO(), tw, htmlPublishCandidate{
|
|
RelPath: "x.html",
|
|
AbsPath: "/nonexistent-path-for-test/x.html",
|
|
Size: 0,
|
|
})
|
|
if err == nil {
|
|
t.Fatalf("expected error for nonexistent abs path")
|
|
}
|
|
if !strings.Contains(err.Error(), "open") {
|
|
t.Fatalf("expected open error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestWriteHTMLPublishTarEntry_WriteHeaderFailure(t *testing.T) {
|
|
// 在已 close 的 tar.Writer 上写 header → WriteHeader 失败
|
|
dir := t.TempDir()
|
|
file := filepath.Join(dir, "x.html")
|
|
if err := os.WriteFile(file, []byte("x"), 0o644); err != nil {
|
|
t.Fatalf("write fixture: %v", err)
|
|
}
|
|
|
|
tw := tar.NewWriter(io.Discard)
|
|
_ = tw.Close() // 先 close,下次 WriteHeader 必失败
|
|
|
|
err := writeHTMLPublishTarEntry(newTestFIO(), tw, htmlPublishCandidate{
|
|
RelPath: "x.html",
|
|
AbsPath: file,
|
|
Size: 1,
|
|
})
|
|
if err == nil {
|
|
t.Fatalf("expected error when writing to closed tar.Writer")
|
|
}
|
|
if !strings.Contains(err.Error(), "write header") {
|
|
t.Fatalf("expected 'write header' error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestWriteHTMLPublishTarEntry_CopyFailure(t *testing.T) {
|
|
// 注入一个 Read 必失败的 fileio.File,让 io.Copy 在 tar 写入阶段出错。
|
|
// 避免 chmod 0o000 的跨平台 / root 用户 flake。
|
|
fio := readFailingFIO{readErr: errors.New("synthetic read failure")}
|
|
tw := tar.NewWriter(io.Discard)
|
|
defer tw.Close()
|
|
|
|
err := writeHTMLPublishTarEntry(fio, tw, htmlPublishCandidate{
|
|
RelPath: "x.html",
|
|
AbsPath: "fixtures/x.html", // 任意路径,Open 由 stub 接管
|
|
Size: 7,
|
|
})
|
|
if err == nil {
|
|
t.Fatalf("expected error when underlying Read fails")
|
|
}
|
|
if !strings.Contains(err.Error(), "copy") {
|
|
t.Fatalf("expected copy-stage error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestBuildHTMLPublishTarball_EntryWriteFailureReturnsError(t *testing.T) {
|
|
// candidate 指向不存在文件 → writeHTMLPublishTarEntry 失败
|
|
// → buildHTMLPublishTarball 返回 nil tarball + error。
|
|
candidates := []htmlPublishCandidate{
|
|
{RelPath: "x.html", AbsPath: "/nonexistent-path-for-test/x.html", Size: 0},
|
|
}
|
|
|
|
tarball, err := buildHTMLPublishTarball(newTestFIO(), candidates)
|
|
if err == nil {
|
|
t.Fatalf("expected error, got tarball=%+v", tarball)
|
|
}
|
|
if tarball != nil {
|
|
t.Fatalf("expected nil tarball on error, got %+v", tarball)
|
|
}
|
|
}
|
|
|
|
func TestWriteHTMLPublishTarEntry_RejectsPathTraversal(t *testing.T) {
|
|
tw := tar.NewWriter(io.Discard)
|
|
defer tw.Close()
|
|
|
|
cases := []struct {
|
|
name string
|
|
rel string
|
|
}{
|
|
{"parent traversal", "../etc/passwd"},
|
|
{"absolute path", "/etc/passwd"},
|
|
{"embedded traversal", "a/../../etc/passwd"},
|
|
{"null byte", "evil\x00.html"},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
err := writeHTMLPublishTarEntry(newTestFIO(), tw, htmlPublishCandidate{
|
|
RelPath: c.rel,
|
|
AbsPath: "fixtures/whatever",
|
|
Size: 0,
|
|
})
|
|
if err == nil {
|
|
t.Fatalf("expected error for RelPath=%q", c.rel)
|
|
}
|
|
if !strings.Contains(err.Error(), "invalid tar entry name") {
|
|
t.Fatalf("expected 'invalid tar entry name' error, got %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|