mirror of
https://github.com/larksuite/cli.git
synced 2026-07-07 00:55:53 +08:00
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>
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)
|
||
}
|
||
})
|
||
}
|
||
}
|