Files
larksuite--cli/tests/cli_e2e/apps/apps_html_publish_dryrun_test.go
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:22:54 +08:00

327 lines
11 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package apps
import (
"context"
"os"
"path/filepath"
"testing"
"time"
clie2e "github.com/larksuite/cli/tests/cli_e2e"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
)
// TestAppsHTMLPublishDryRun exercises the walker / manifest layer without
// packing or uploading. --path goes through LocalFileIO which bounds reads to
// the runtime cwd, so each sub-test seeds fixtures in a t.TempDir and runs
// the binary with WorkDir set to that dir + relative --path.
//
// Hidden files are intentionally included — the walker is deliberately not
// filtering, so the manifest must reflect everything the user pointed --path
// at. Users are documented to pass clean build output directories (e.g.
// ./dist), not source trees.
func TestAppsHTMLPublishDryRun(t *testing.T) {
setAppsDryRunEnv(t)
t.Run("Directory_ReportsManifest", func(t *testing.T) {
dir := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(dir, "dist"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "dist", "index.html"), []byte("<html><body>hi</body></html>"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(dir, "dist", "logo.svg"), []byte("<svg/>"), 0o644))
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
t.Cleanup(cancel)
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"apps", "+html-publish",
"--app-id", "app_x",
"--path", "./dist",
"--dry-run",
},
DefaultAs: "user",
WorkDir: dir,
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
assert.Equal(t, "POST", gjson.Get(result.Stdout, "api.0.method").String())
assert.Equal(t, "/open-apis/spark/v1/apps/app_x/upload_and_release_html_code", gjson.Get(result.Stdout, "api.0.url").String())
// file_count / files / total_size_bytes sit at envelope top level
// (not under api.0.body — manifest is dry-run metadata, not the HTTP body).
assert.Equal(t, int64(2), gjson.Get(result.Stdout, "file_count").Int())
assert.Greater(t, gjson.Get(result.Stdout, "total_size_bytes").Int(), int64(0))
files := gjson.Get(result.Stdout, "files").Array()
require.Len(t, files, 2)
names := []string{files[0].String(), files[1].String()}
assert.Contains(t, names, "index.html")
assert.Contains(t, names, "logo.svg")
})
t.Run("SingleFile_OneEntry", func(t *testing.T) {
dir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(dir, "page.html"), []byte("<html></html>"), 0o644))
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
t.Cleanup(cancel)
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"apps", "+html-publish",
"--app-id", "app_x",
"--path", "page.html",
"--dry-run",
},
DefaultAs: "user",
WorkDir: dir,
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
assert.Equal(t, int64(1), gjson.Get(result.Stdout, "file_count").Int())
assert.Equal(t, "page.html", gjson.Get(result.Stdout, "files.0").String())
})
t.Run("HiddenFilesIncludedExceptGit", func(t *testing.T) {
// The walker filters the .git directory (and a .git gitdir pointer file)
// so a stray repo under --path doesn't ship its history / remote URL to a
// public share URL. Generic hidden files like .DS_Store are NOT filtered —
// only .git is — so users still see everything else they pointed --path at.
dir := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(dir, "dist"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "dist", "index.html"), []byte("<html/>"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(dir, "dist", ".DS_Store"), []byte("noise"), 0o644))
require.NoError(t, os.Mkdir(filepath.Join(dir, "dist", ".git"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "dist", ".git", "HEAD"), []byte("ref: refs/heads/main\n"), 0o644))
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
t.Cleanup(cancel)
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"apps", "+html-publish",
"--app-id", "app_x",
"--path", "./dist",
"--dry-run",
},
DefaultAs: "user",
WorkDir: dir,
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
// index.html + .DS_Store kept; .git/HEAD filtered out → 2 files.
assert.Equal(t, int64(2), gjson.Get(result.Stdout, "file_count").Int(),
"walker must keep non-.git hidden files but drop .git; got: %s", result.Stdout)
names := gjson.Get(result.Stdout, "files").Array()
var got []string
for _, n := range names {
got = append(got, n.String())
}
assert.Contains(t, got, "index.html")
assert.Contains(t, got, ".DS_Store")
assert.NotContains(t, got, ".git/HEAD", "walker must exclude .git contents")
})
t.Run("EmptyDir_ManifestEmpty", func(t *testing.T) {
dir := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(dir, "dist"), 0o755))
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
t.Cleanup(cancel)
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"apps", "+html-publish",
"--app-id", "app_x",
"--path", "./dist",
"--dry-run",
},
DefaultAs: "user",
WorkDir: dir,
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
assert.Equal(t, int64(0), gjson.Get(result.Stdout, "file_count").Int())
assert.Equal(t, int64(0), gjson.Get(result.Stdout, "total_size_bytes").Int())
assert.Contains(t, gjson.Get(result.Stdout, "validation_error").String(), "index.html",
"empty dir should report index.html validation_error: %s", result.Stdout)
})
t.Run("MissingIndexHTML_SurfacesValidationError", func(t *testing.T) {
dir := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(dir, "dist"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "dist", "page.html"), []byte("<html/>"), 0o644))
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
t.Cleanup(cancel)
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"apps", "+html-publish",
"--app-id", "app_x",
"--path", "./dist",
"--dry-run",
},
DefaultAs: "user",
WorkDir: dir,
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
assert.Equal(t, int64(1), gjson.Get(result.Stdout, "file_count").Int())
assert.Equal(t, "page.html", gjson.Get(result.Stdout, "files.0").String())
assert.Contains(t, gjson.Get(result.Stdout, "validation_error").String(), "index.html")
})
t.Run("RejectsMissingAppID", func(t *testing.T) {
dir := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(dir, "dist"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "dist", "index.html"), []byte("<html/>"), 0o644))
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
t.Cleanup(cancel)
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"apps", "+html-publish",
"--path", "./dist",
"--dry-run",
},
DefaultAs: "user",
WorkDir: dir,
})
require.NoError(t, err)
result.AssertExitCode(t, 2)
assert.Contains(t, validateErrorMessage(result), `required flag(s) "app-id" not set`)
})
t.Run("RejectsMissingPath", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
t.Cleanup(cancel)
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"apps", "+html-publish",
"--app-id", "app_x",
"--dry-run",
},
DefaultAs: "user",
})
require.NoError(t, err)
result.AssertExitCode(t, 2)
assert.Contains(t, validateErrorMessage(result), `required flag(s) "path" not set`)
})
t.Run("RejectsSensitivePathsByDefault", func(t *testing.T) {
// Validate scans candidates for well-known credential files and rejects
// when any are found. Dry-run also fails (Validate runs before the
// dry-run branch) — that's the point: dry-run preview matches Execute.
dir := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(dir, "dist"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "dist", "index.html"), []byte("<html/>"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(dir, "dist", ".env"), []byte("SECRET=xxx\n"), 0o644))
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
t.Cleanup(cancel)
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"apps", "+html-publish",
"--app-id", "app_x",
"--path", "./dist",
"--dry-run",
},
DefaultAs: "user",
WorkDir: dir,
})
require.NoError(t, err)
result.AssertExitCode(t, 2)
assert.Contains(t, validateErrorMessage(result), ".env",
"validation error should name the offending file: %s", result.Stderr)
})
t.Run("AllowSensitiveOverride", func(t *testing.T) {
// --allow-sensitive bypasses the credential-file gate (legitimate
// cases: docs site shipping example .env files). Dry-run output
// surfaces a sensitive_waived field so the caller still sees what
// was let through.
dir := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(dir, "dist"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "dist", "index.html"), []byte("<html/>"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(dir, "dist", ".env.example"), []byte("API_KEY=replace-me\n"), 0o644))
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
t.Cleanup(cancel)
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"apps", "+html-publish",
"--app-id", "app_x",
"--path", "./dist",
"--allow-sensitive",
"--dry-run",
},
DefaultAs: "user",
WorkDir: dir,
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
waived := gjson.Get(result.Stdout, "sensitive_waived").Array()
require.Len(t, waived, 1, "expected sensitive_waived to list the file, got: %s", result.Stdout)
assert.Equal(t, ".env.example", waived[0].String())
})
t.Run("CleanCwdAllowed", func(t *testing.T) {
// --path "." is no longer hard-rejected. A cwd that doesn't contain
// well-known credential files is a valid publish target.
dir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(dir, "index.html"), []byte("<html/>"), 0o644))
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
t.Cleanup(cancel)
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"apps", "+html-publish",
"--app-id", "app_x",
"--path", ".",
"--dry-run",
},
DefaultAs: "user",
WorkDir: dir,
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
assert.Equal(t, int64(1), gjson.Get(result.Stdout, "file_count").Int())
})
t.Run("TrimsAppIDAndPath", func(t *testing.T) {
dir := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(dir, "dist"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "dist", "index.html"), []byte("<html/>"), 0o644))
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
t.Cleanup(cancel)
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"apps", "+html-publish",
"--app-id", " app_x ",
"--path", " ./dist ",
"--dry-run",
},
DefaultAs: "user",
WorkDir: dir,
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
assert.Equal(t, "/open-apis/spark/v1/apps/app_x/upload_and_release_html_code",
gjson.Get(result.Stdout, "api.0.url").String())
assert.Equal(t, int64(1), gjson.Get(result.Stdout, "file_count").Int(),
"path trimming must produce the same manifest as untrimmed input")
})
}