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

299 lines
9.4 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package apps
import (
"encoding/json"
"strings"
"testing"
"time"
"github.com/larksuite/cli/internal/httpmock"
)
func TestMetricNamesMapping(t *testing.T) {
got, labels, err := metricNamesForCLI("requests", "")
if err != nil {
t.Fatal(err)
}
if strings.Join(got, ",") != "client_api_request_count,client_api_request_error_count" {
t.Fatalf("names = %#v", got)
}
if strings.Join(labels, ",") != "total,error" {
t.Fatalf("labels = %#v", labels)
}
if _, _, err := metricNamesForCLI("cpu", "p99"); err == nil {
t.Fatalf("cpu with p99 should fail")
}
}
func TestAppsMetricList_DryRunUsesSeconds(t *testing.T) {
factory, stdout, _ := newAppsExecuteFactory(t)
err := runAppsShortcut(t, AppsMetricList, []string{
"+metric-list", "--app-id", "app_x", "--metric", "requests",
"--series", "total", "--since", "2026-06-23T10:00:00Z",
"--until", "2026-06-23T10:01:00Z", "--down-sample", "1m",
"--dry-run", "--as", "user",
}, factory, stdout)
if err != nil {
t.Fatalf("dry-run err=%v", err)
}
var env struct {
API []struct {
Method string `json:"method"`
URL string `json:"url"`
Body map[string]interface{} `json:"body"`
} `json:"api"`
}
if err := json.Unmarshal(stdout.Bytes(), &env); err != nil {
t.Fatalf("decode dry-run: %v\n%s", err, stdout.String())
}
if env.API[0].Method != "POST" || env.API[0].URL != "/open-apis/spark/v1/apps/app_x/query_metrics_data" {
t.Fatalf("method/url = %s %s", env.API[0].Method, env.API[0].URL)
}
body := env.API[0].Body
if _, ok := body["start_timestamp"]; !ok {
t.Fatalf("metric dry-run missing start_timestamp: %#v", body)
}
if _, ok := body["start_timestamp_ns"]; ok {
t.Fatalf("metric should not use start_timestamp_ns: %#v", body)
}
if _, ok := body["app_env"]; ok {
t.Fatalf("metric OpenAPI body should not include app_env: %#v", body)
}
if body["start_timestamp"] != "1782208800" || body["end_timestamp"] != "1782208860" {
t.Fatalf("metric timestamps = %v %v", body["start_timestamp"], body["end_timestamp"])
}
if body["down_sample"] != "1m" {
t.Fatalf("down_sample = %v", body["down_sample"])
}
}
func TestAppsMetricList_AutoDownSampleByRange(t *testing.T) {
for _, tc := range []struct {
name string
since string
until string
want string
}{
{name: "short", since: "2026-06-23T10:00:00Z", until: "2026-06-23T12:00:00Z", want: "1m"},
{name: "medium", since: "2026-06-21T10:00:00Z", until: "2026-06-23T10:00:00Z", want: "1h"},
{name: "long", since: "2026-06-01T10:00:00Z", until: "2026-06-23T10:00:00Z", want: "1d"},
} {
t.Run(tc.name, func(t *testing.T) {
factory, stdout, _ := newAppsExecuteFactory(t)
err := runAppsShortcut(t, AppsMetricList, []string{
"+metric-list", "--app-id", "app_x", "--metric", "requests",
"--since", tc.since, "--until", tc.until, "--dry-run", "--as", "user",
}, factory, stdout)
if err != nil {
t.Fatalf("dry-run err=%v", err)
}
var env struct {
API []struct {
Body map[string]interface{} `json:"body"`
} `json:"api"`
}
if err := json.Unmarshal(stdout.Bytes(), &env); err != nil {
t.Fatalf("decode dry-run: %v\n%s", err, stdout.String())
}
if got := env.API[0].Body["down_sample"]; got != tc.want {
t.Fatalf("down_sample = %#v, want %q; stdout:\n%s", got, tc.want, stdout.String())
}
})
}
}
func TestAppsMetricList_RejectsDevEnv(t *testing.T) {
factory, stdout, _ := newAppsExecuteFactory(t)
err := runAppsShortcut(t, AppsMetricList, []string{
"+metric-list", "--app-id", "app_x", "--metric", "requests", "--environment", "dev", "--as", "user",
}, factory, stdout)
requireAppsValidationParam(t, err, "--environment")
}
func TestAppsMetricList_FillsMissingRequestValuesWithZero(t *testing.T) {
factory, stdout, reg := newAppsExecuteFactory(t)
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/spark/v1/apps/app_x/query_metrics_data",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"points": []interface{}{
map[string]interface{}{
"timestamp": float64(1782208800),
"dimensions": map[string]interface{}{"page": "/home"},
"values": []interface{}{
map[string]interface{}{"metric_name": "client_api_request_count", "value": float64(12)},
},
},
map[string]interface{}{
"timestamp": float64(1782208860),
"dimensions": map[string]interface{}{"page": "/settings"},
"values": []interface{}{
map[string]interface{}{"metric_name": "client_api_request_count", "value": float64(8)},
map[string]interface{}{"metric_name": "client_api_request_error_count", "value": nil},
},
},
},
},
},
})
if err := runAppsShortcut(t, AppsMetricList, []string{
"+metric-list", "--app-id", "app_x", "--metric", "requests", "--as", "user",
}, factory, stdout); err != nil {
t.Fatalf("execute err=%v", err)
}
var env struct {
Data struct {
Items []struct {
Values map[string]interface{} `json:"values"`
} `json:"items"`
HasMore bool `json:"has_more"`
} `json:"data"`
}
if err := json.Unmarshal(stdout.Bytes(), &env); err != nil {
t.Fatalf("decode output: %v\n%s", err, stdout.String())
}
if env.Data.HasMore {
t.Fatalf("has_more = true, want false")
}
if len(env.Data.Items) != 2 {
t.Fatalf("items len = %d", len(env.Data.Items))
}
for i, item := range env.Data.Items {
if item.Values["error"] != float64(0) {
t.Fatalf("item %d error = %#v, want 0; values=%#v", i, item.Values["error"], item.Values)
}
}
}
func TestAppsMetricList_PrettyFormatsTimeFirst(t *testing.T) {
const rawSec = int64(1782208800)
factory, stdout, reg := newAppsExecuteFactory(t)
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/spark/v1/apps/app_x/query_metrics_data",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"points": []interface{}{
map[string]interface{}{
"timestamp": float64(rawSec),
"values": []interface{}{
map[string]interface{}{"metric_name": "client_api_request_count", "value": float64(12)},
map[string]interface{}{"metric_name": "client_api_request_error_count", "value": float64(1)},
},
},
},
},
},
})
if err := runAppsShortcut(t, AppsMetricList, []string{
"+metric-list", "--app-id", "app_x", "--metric", "requests", "--format", "pretty", "--as", "user",
}, factory, stdout); err != nil {
t.Fatalf("execute err=%v", err)
}
got := stdout.String()
wantTime := time.Unix(rawSec, 0).Local().Format("2006-01-02 15:04:05")
if !strings.HasPrefix(got, "time") {
t.Fatalf("pretty output should start with time column, got:\n%s", got)
}
if !strings.Contains(got, wantTime) {
t.Fatalf("pretty output missing formatted time %q:\n%s", wantTime, got)
}
if strings.Contains(got, "timestamp") || strings.Contains(got, "1782208800") {
t.Fatalf("pretty output should hide raw timestamp, got:\n%s", got)
}
}
func TestAppsMetricList_NamedSeriesDoesNotDependOnBackendOrder(t *testing.T) {
factory, stdout, reg := newAppsExecuteFactory(t)
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/spark/v1/apps/app_x/query_metrics_data",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"series": []interface{}{
map[string]interface{}{
"name": "client_api_request_error_count",
"points": []interface{}{
map[string]interface{}{"timestamp": float64(1782208800), "value": float64(2)},
},
},
map[string]interface{}{
"name": "client_api_request_count",
"points": []interface{}{
map[string]interface{}{"timestamp": float64(1782208800), "value": float64(10)},
},
},
},
},
},
})
if err := runAppsShortcut(t, AppsMetricList, []string{
"+metric-list", "--app-id", "app_x", "--metric", "requests", "--as", "user",
}, factory, stdout); err != nil {
t.Fatalf("execute err=%v", err)
}
var env struct {
Data struct {
Items []struct {
Values map[string]interface{} `json:"values"`
} `json:"items"`
} `json:"data"`
}
if err := json.Unmarshal(stdout.Bytes(), &env); err != nil {
t.Fatalf("decode output: %v\n%s", err, stdout.String())
}
if len(env.Data.Items) != 1 {
t.Fatalf("items len = %d", len(env.Data.Items))
}
values := env.Data.Items[0].Values
if values["total"] != float64(10) || values["error"] != float64(2) {
t.Fatalf("values = %#v, want total=10 error=2", values)
}
}
func TestAppsMetricList_EmptyResponseOutputsEmptyItemsArray(t *testing.T) {
factory, stdout, reg := newAppsExecuteFactory(t)
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/spark/v1/apps/app_x/query_metrics_data",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{},
},
})
if err := runAppsShortcut(t, AppsMetricList, []string{
"+metric-list", "--app-id", "app_x", "--metric", "latency", "--as", "user",
}, factory, stdout); err != nil {
t.Fatalf("execute err=%v", err)
}
var env struct {
Data struct {
Items []map[string]interface{} `json:"items"`
HasMore bool `json:"has_more"`
} `json:"data"`
}
if err := json.Unmarshal(stdout.Bytes(), &env); err != nil {
t.Fatalf("decode output: %v\n%s", err, stdout.String())
}
if env.Data.Items == nil {
t.Fatalf("items decoded as nil; stdout=%s", stdout.String())
}
if len(env.Data.Items) != 0 || env.Data.HasMore {
t.Fatalf("empty output = items %#v has_more %v", env.Data.Items, env.Data.HasMore)
}
}