Files
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

253 lines
9.2 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 (
"encoding/json"
"errors"
"strings"
"testing"
"time"
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/httpmock"
)
// 设计原则三:<timestamp> 四种格式 → 统一 RFC3339 UTC。
func TestNormalizeTimestamp_AllFormats(t *testing.T) {
// 空串透传
if got, err := normalizeTimestamp(" "); err != nil || got != "" {
t.Fatalf("empty → %q,%v want \"\",nil", got, err)
}
// ISO 8601 带 TZZ 原样、显式偏移换算到 UTC
mustEq := func(in, want string) {
got, err := normalizeTimestamp(in)
if err != nil || got != want {
t.Errorf("normalizeTimestamp(%q)=%q,%v want %q", in, got, err, want)
}
}
mustEq("2026-04-15T10:00:00Z", "2026-04-15T10:00:00Z")
mustEq("2026-04-15T10:00:00+08:00", "2026-04-15T02:00:00Z") // +08:00 → UTC -8h
// date / local datetime:按本地时区解释再转 UTC(与 time.ParseInLocation 对齐)
dExp, _ := time.ParseInLocation("2006-01-02", "2026-04-15", time.Local)
mustEq("2026-04-15", dExp.UTC().Format(time.RFC3339))
ldExp, _ := time.ParseInLocation("2006-01-02T15:04:05", "2026-04-15T10:00:00", time.Local)
mustEq("2026-04-15T10:00:00", ldExp.UTC().Format(time.RFC3339))
// 相对:从现在往前推,结果应 ≈ now-dur(5s 容差)
for _, c := range []struct {
in string
dur time.Duration
}{{"30s", 30 * time.Second}, {"5m", 5 * time.Minute}, {"2h", 2 * time.Hour}, {"3d", 72 * time.Hour}, {"1w", 7 * 24 * time.Hour}} {
got, err := normalizeTimestamp(c.in)
if err != nil {
t.Errorf("normalizeTimestamp(%q) err=%v", c.in, err)
continue
}
ts, perr := time.Parse(time.RFC3339, got)
if perr != nil {
t.Errorf("normalizeTimestamp(%q)=%q not RFC3339", c.in, got)
continue
}
want := time.Now().Add(-c.dur)
if diff := want.Sub(ts); diff > 5*time.Second || diff < -5*time.Second {
t.Errorf("normalizeTimestamp(%q)=%q off by %v from now-%v", c.in, got, diff, c.dur)
}
}
// 非法格式 → error
for _, bad := range []string{"notatime", "7x", "2026/04/15", "2026-13-99"} {
if _, err := normalizeTimestamp(bad); err == nil {
t.Errorf("normalizeTimestamp(%q) expected error", bad)
}
}
}
const fileListURL = "/open-apis/spark/v1/apps/app_x/storage/file_list"
// TestAppsFileList_RequiresAppID 验证空白 --app-id 触发 --app-id typed 校验错误。
func TestAppsFileList_RequiresAppID(t *testing.T) {
factory, stdout, _ := newAppsExecuteFactory(t)
err := runAppsShortcut(t, AppsFileList,
[]string{"+file-list", "--app-id", " ", "--as", "user"}, factory, stdout)
var ve *errs.ValidationError
if !errors.As(err, &ve) {
t.Fatalf("err = %T %v, want *errs.ValidationError", err, err)
}
if ve.Param != "--app-id" {
t.Fatalf("Param = %q, want --app-id", ve.Param)
}
}
// 过滤器 + 分页全部进 querysize-gt/lt 走 intuploaded_since/until 原样)。
func TestAppsFileList_DryRunSendsFiltersAndPagination(t *testing.T) {
factory, stdout, _ := newAppsExecuteFactory(t)
if err := runAppsShortcut(t, AppsFileList,
[]string{"+file-list", "--app-id", "app_x",
"--name", "logo.png", "--path", "/x.png", "--type", "image/png",
"--size-gt", "100", "--size-lt", "9000",
"--uploaded-since", "2026-01-01", "--uploaded-until", "2026-02-01",
"--page-size", "5", "--page-token", "cur-1",
"--dry-run", "--as", "user"},
factory, stdout); err != nil {
t.Fatalf("dry-run err=%v", err)
}
var env struct {
API []struct {
Method string `json:"method"`
URL string `json:"url"`
Params map[string]interface{} `json:"params"`
} `json:"api"`
}
if err := json.Unmarshal([]byte(stdout.String()), &env); err != nil {
t.Fatalf("decode dry-run: %v\n%s", err, stdout.String())
}
a := env.API[0]
if a.Method != "GET" || a.URL != fileListURL {
t.Fatalf("method/url = %s %s", a.Method, a.URL)
}
// 设计原则三:date 入参会被归一化为 RFC3339 UTC,期望值用 normalizeTimestamp 计算(避开本地时区脆弱断言)。
sinceN, _ := normalizeTimestamp("2026-01-01")
untilN, _ := normalizeTimestamp("2026-02-01")
wantStr := map[string]string{
"name": "logo.png", "path": "/x.png", "type": "image/png",
"uploaded_since": sinceN, "uploaded_until": untilN, "page_token": "cur-1",
}
for k, v := range wantStr {
if a.Params[k] != v {
t.Errorf("params.%s = %v, want %v", k, a.Params[k], v)
}
}
// 且确实归一化成了 UTC(以 Z 结尾),不是原样透传。
if s, _ := a.Params["uploaded_since"].(string); !strings.HasSuffix(s, "Z") {
t.Errorf("uploaded_since not normalized to RFC3339 UTC: %v", a.Params["uploaded_since"])
}
for _, k := range []string{"size_gt", "size_lt", "page_size"} {
if _, ok := a.Params[k]; !ok {
t.Errorf("params missing %s: %v", k, a.Params)
}
}
}
// 0 值过滤器不下发(size-gt/lt 缺省 0、空字符串过滤器)。
func TestAppsFileList_DryRunOmitsEmptyFilters(t *testing.T) {
factory, stdout, _ := newAppsExecuteFactory(t)
if err := runAppsShortcut(t, AppsFileList,
[]string{"+file-list", "--app-id", "app_x", "--dry-run", "--as", "user"}, factory, stdout); err != nil {
t.Fatalf("dry-run err=%v", err)
}
var env struct {
API []struct {
Params map[string]interface{} `json:"params"`
} `json:"api"`
}
_ = json.Unmarshal([]byte(stdout.String()), &env)
for _, banned := range []string{"name", "path", "type", "size_gt", "size_lt", "uploaded_since", "uploaded_until", "page_token"} {
if _, ok := env.API[0].Params[banned]; ok {
t.Errorf("params should omit empty %s: %v", banned, env.API[0].Params)
}
}
if _, ok := env.API[0].Params["page_size"]; !ok {
t.Errorf("params should always carry page_size: %v", env.API[0].Params)
}
}
// created_at/created_by → uploaded_at/uploaded_bycreated_by 是 JSON 字符串 → parse 成对象。
func TestAppsFileList_SuccessProjectsCreatedToUploaded(t *testing.T) {
factory, stdout, reg := newAppsExecuteFactory(t)
reg.Register(&httpmock.Stub{
Method: "GET",
URL: fileListURL,
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"has_more": false,
"page_token": "",
"items": []interface{}{
map[string]interface{}{
"file_name": "logo.png",
"path": "/1858537546760216.png",
"size_bytes": 24580,
"type": "image/png",
"created_at": "2026-04-15T10:30:00Z",
"created_by": `{"id":"7311","name":"alice"}`,
"download_url": "/spark/app/x/1858537546760216.png",
},
},
},
},
})
if err := runAppsShortcut(t, AppsFileList,
[]string{"+file-list", "--app-id", "app_x", "--as", "user"}, factory, stdout); err != nil {
t.Fatalf("execute err=%v", err)
}
got := stdout.String()
for _, want := range []string{`"uploaded_at": "2026-04-15T10:30:00Z"`, `"uploaded_by"`, `"name": "alice"`, `"id": "7311"`} {
if !strings.Contains(got, want) {
t.Errorf("stdout missing %q:\n%s", want, got)
}
}
// created_* 不应再出现在输出。
for _, banned := range []string{"created_at", "created_by"} {
if strings.Contains(got, banned) {
t.Errorf("stdout should not contain %q (renamed to uploaded_*):\n%s", banned, got)
}
}
}
// TestAppsFileList_PrettyTableAndEmpty 验证 pretty 非空时渲染表头与人类可读 size,空结果时输出 "No files found."。
func TestAppsFileList_PrettyTableAndEmpty(t *testing.T) {
// 非空:5 列表头。
factory, stdout, reg := newAppsExecuteFactory(t)
reg.Register(&httpmock.Stub{
Method: "GET", URL: fileListURL,
Body: map[string]interface{}{"code": 0, "data": map[string]interface{}{
"items": []interface{}{map[string]interface{}{
"file_name": "logo.png", "path": "/x.png", "size_bytes": 24576, "type": "image/png",
"created_at": "2026-04-15T10:30:00Z",
}},
}},
})
if err := runAppsShortcut(t, AppsFileList,
[]string{"+file-list", "--app-id", "app_x", "--format", "pretty", "--as", "user"}, factory, stdout); err != nil {
t.Fatalf("execute err=%v", err)
}
got := stdout.String()
if !strings.Contains(got, "file_name") || !strings.Contains(got, "uploaded_at") || !strings.Contains(got, "24 KB") {
t.Fatalf("pretty table malformed:\n%s", got)
}
// 空:No files found.
factory2, stdout2, reg2 := newAppsExecuteFactory(t)
reg2.Register(&httpmock.Stub{
Method: "GET", URL: fileListURL,
Body: map[string]interface{}{"code": 0, "data": map[string]interface{}{"items": []interface{}{}}},
})
if err := runAppsShortcut(t, AppsFileList,
[]string{"+file-list", "--app-id", "app_x", "--format", "pretty", "--as", "user"}, factory2, stdout2); err != nil {
t.Fatalf("execute err=%v", err)
}
if !strings.Contains(stdout2.String(), "No files found.") {
t.Fatalf("empty pretty should say 'No files found.', got: %s", stdout2.String())
}
}
// TestParseFileUser_Cases 验证 parseFileUser:合法 JSON 解析成对象,空串/非法/全空字段均返回 nil。
func TestParseFileUser_Cases(t *testing.T) {
if u := parseFileUser(`{"id":"1","name":"a"}`); u == nil || u.ID != "1" || u.Name != "a" {
t.Fatalf("valid parse failed: %#v", u)
}
if u := parseFileUser(""); u != nil {
t.Errorf("empty → nil, got %#v", u)
}
if u := parseFileUser("not json"); u != nil {
t.Errorf("invalid → nil, got %#v", u)
}
if u := parseFileUser(`{"id":"","name":""}`); u != nil {
t.Errorf("all-empty → nil, got %#v", u)
}
}