Files
larksuite-cli/shortcuts/apps/html_publish_tarball_test.go
raistlin042 6cea6c9af0 feat(apps): add miaoda apps domain (6 shortcuts + dry-run e2e) (#1002)
Adds the apps domain to lark-cli for managing Miaoda (妙搭) applications: 6 shortcuts covering the full lifecycle (+create / +update / +list / +access-scope-set / +access-scope-get / +html-publish). Aligned with the OAPI v2 design — app_type enum (currently HTML), string scope enum (All / Tenant / Range), cursor pagination, in-memory tar.gz multipart publish flow. Namespace registered at /open-apis/spark/v1/ with spark:app.* scopes.

---------

Co-authored-by: wangjiangwen-gif <286006750+wangjiangwen-gif@users.noreply.github.com>
2026-05-21 20:30:42 +08:00

194 lines
5.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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)
}
})
}
}