bf9395e022
CI / results (push) Blocked by required conditions
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / e2e-live (push) Waiting to run
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 / deadcode (push) Waiting to run
665 lines
22 KiB
Go
665 lines
22 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 TestAppsLogList_DryRunBuildsSearchLogsBody(t *testing.T) {
|
|
factory, stdout, _ := newAppsExecuteFactory(t)
|
|
err := runAppsShortcut(t, AppsLogList, []string{
|
|
"+log-list", "--app-id", "app_x", "--level", "error",
|
|
"--trace-id", "trace-1",
|
|
"--keyword", "timeout", "--module", "frontend", "--user-id", "ou_1",
|
|
"--page", "/home", "--api", "/api/orders", "--min-duration", "200",
|
|
"--since", "2026-06-23T10:00:00Z", "--until", "2026-06-23T10:01:00Z",
|
|
"--page-size", "20", "--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/search_logs" {
|
|
t.Fatalf("method/url = %s %s", env.API[0].Method, env.API[0].URL)
|
|
}
|
|
if env.API[0].Body["app_env"] != "runtime" || env.API[0].Body["limit"] != float64(20) {
|
|
t.Fatalf("body = %#v", env.API[0].Body)
|
|
}
|
|
filter := env.API[0].Body["filter"].(map[string]interface{})
|
|
if got := filter["keyword"]; got != "timeout" {
|
|
t.Fatalf("filter.keyword = %v", got)
|
|
}
|
|
for key, want := range map[string]string{
|
|
"modules": "frontend",
|
|
"user_ids": "ou_1",
|
|
"pages": "/home",
|
|
"apis": "/api/orders",
|
|
} {
|
|
values, ok := filter[key].([]interface{})
|
|
if !ok || len(values) != 1 || values[0] != want {
|
|
t.Fatalf("filter.%s = %#v, want [%q]", key, filter[key], want)
|
|
}
|
|
}
|
|
if env.API[0].Body["start_timestamp_ns"] != "1782208800000000000" ||
|
|
env.API[0].Body["end_timestamp_ns"] != "1782208860000000000" {
|
|
t.Fatalf("timestamps = %#v %#v", env.API[0].Body["start_timestamp_ns"], env.API[0].Body["end_timestamp_ns"])
|
|
}
|
|
}
|
|
|
|
func TestAppsLogList_DoesNotAcceptLogIDFlag(t *testing.T) {
|
|
factory, stdout, _ := newAppsExecuteFactory(t)
|
|
err := runAppsShortcut(t, AppsLogList, []string{
|
|
"+log-list", "--app-id", "app_x", "--log-id", "LOG1", "--as", "user",
|
|
}, factory, stdout)
|
|
if err == nil || !strings.Contains(err.Error(), "unknown flag: --log-id") {
|
|
t.Fatalf("expected unknown --log-id flag, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAppsLogList_RejectsDevEnv(t *testing.T) {
|
|
factory, stdout, _ := newAppsExecuteFactory(t)
|
|
err := runAppsShortcut(t, AppsLogList, []string{"+log-list", "--app-id", "app_x", "--environment", "dev", "--as", "user"}, factory, stdout)
|
|
requireAppsValidationParam(t, err, "--environment")
|
|
}
|
|
|
|
func TestAppsLogGet_SearchesByLogIDLimitOne(t *testing.T) {
|
|
factory, stdout, reg := newAppsExecuteFactory(t)
|
|
stub := &httpmock.Stub{
|
|
Method: "POST",
|
|
URL: "/open-apis/spark/v1/apps/app_x/search_logs",
|
|
Body: map[string]interface{}{
|
|
"code": 0,
|
|
"data": map[string]interface{}{
|
|
"log_items": []interface{}{
|
|
map[string]interface{}{"log_id": "LOG1", "level": "INFO"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
reg.Register(stub)
|
|
if err := runAppsShortcut(t, AppsLogGet, []string{"+log-get", "--app-id", "app_x", "--log-id", "LOG1", "--as", "user"}, factory, stdout); err != nil {
|
|
t.Fatalf("execute err=%v", err)
|
|
}
|
|
var sent map[string]interface{}
|
|
if err := json.Unmarshal(stub.CapturedBody, &sent); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if sent["limit"] != float64(1) {
|
|
t.Fatalf("limit = %v, want 1", sent["limit"])
|
|
}
|
|
if sent["app_env"] != "runtime" {
|
|
t.Fatalf("app_env = %v, want runtime", sent["app_env"])
|
|
}
|
|
}
|
|
|
|
func TestAppsLogGet_AcceptsDataArraySearchResponse(t *testing.T) {
|
|
factory, stdout, reg := newAppsExecuteFactory(t)
|
|
search := &httpmock.Stub{
|
|
Method: "POST",
|
|
URL: "/open-apis/spark/v1/apps/app_x/search_logs",
|
|
RawBody: []byte(`{
|
|
"code": 0,
|
|
"data": [
|
|
{
|
|
"log_id": "LOG7655249917057764881",
|
|
"level": "ERROR",
|
|
"attributes": {
|
|
"commit_id": "commit_array",
|
|
"source_map_file_prefix": "sourcemaps/array",
|
|
"frames": [{"file":"main.js","line":10,"column":20}]
|
|
}
|
|
}
|
|
]
|
|
}`),
|
|
}
|
|
resolve := &httpmock.Stub{
|
|
Method: "POST",
|
|
URL: "/open-apis/spark/v1/apps/app_x/resolve_stack_trace",
|
|
Body: map[string]interface{}{
|
|
"code": 0,
|
|
"data": map[string]interface{}{
|
|
"source_stack": []interface{}{
|
|
map[string]interface{}{"file": "src/App.tsx", "line": 7, "column": 9},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
reg.Register(search)
|
|
reg.Register(resolve)
|
|
|
|
if err := runAppsShortcut(t, AppsLogGet, []string{"+log-get", "--app-id", "app_x", "--log-id", "LOG7655249917057764881", "--as", "user"}, factory, stdout); err != nil {
|
|
t.Fatalf("execute err=%v", err)
|
|
}
|
|
if got := stdout.String(); !strings.Contains(got, `"source_stack_status": "resolved"`) || !strings.Contains(got, "src/App.tsx") {
|
|
t.Fatalf("stdout missing resolved source stack from data array response: %s", got)
|
|
}
|
|
}
|
|
|
|
func TestAppsLogList_NormalizesResponseVariantsAndCanonicalLevel(t *testing.T) {
|
|
factory, stdout, reg := newAppsExecuteFactory(t)
|
|
reg.Register(&httpmock.Stub{
|
|
Method: "POST",
|
|
URL: "/open-apis/spark/v1/apps/app_x/search_logs",
|
|
Body: map[string]interface{}{
|
|
"code": 0,
|
|
"data": map[string]interface{}{
|
|
"logItems": []interface{}{
|
|
map[string]interface{}{
|
|
"id": "LOG1",
|
|
"traceID": "trace-1",
|
|
"timestampNs": "1782209472123456789",
|
|
"severityText": "ERROR",
|
|
},
|
|
},
|
|
"nextPageToken": "tok-next",
|
|
"hasMore": true,
|
|
},
|
|
},
|
|
})
|
|
|
|
if err := runAppsShortcut(t, AppsLogList, []string{"+log-list", "--app-id", "app_x", "--as", "user"}, factory, stdout); err != nil {
|
|
t.Fatalf("execute err=%v", err)
|
|
}
|
|
|
|
var env struct {
|
|
Data struct {
|
|
Items []map[string]interface{} `json:"items"`
|
|
PageToken string `json:"page_token"`
|
|
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.PageToken != "tok-next" || !env.Data.HasMore {
|
|
t.Fatalf("pagination = token %q has_more %v", env.Data.PageToken, env.Data.HasMore)
|
|
}
|
|
if len(env.Data.Items) != 1 {
|
|
t.Fatalf("items len = %d", len(env.Data.Items))
|
|
}
|
|
item := env.Data.Items[0]
|
|
if item["level"] != "ERROR" || item["severity_text"] != "ERROR" || item["severityText"] != "ERROR" {
|
|
t.Fatalf("level fields = %#v", item)
|
|
}
|
|
}
|
|
|
|
func TestAppsLogList_NormalizesKVAttributesToObject(t *testing.T) {
|
|
factory, stdout, reg := newAppsExecuteFactory(t)
|
|
reg.Register(&httpmock.Stub{
|
|
Method: "POST",
|
|
URL: "/open-apis/spark/v1/apps/app_x/search_logs",
|
|
Body: map[string]interface{}{
|
|
"code": 0,
|
|
"data": map[string]interface{}{
|
|
"log_items": []interface{}{
|
|
map[string]interface{}{
|
|
"log_id": "LOG1",
|
|
"attributes": []interface{}{
|
|
map[string]interface{}{"key": "app_env", "value": "runtime"},
|
|
map[string]interface{}{"key": "duration_ms", "value": "8263"},
|
|
map[string]interface{}{"key": "module", "value": "gateway"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
if err := runAppsShortcut(t, AppsLogList, []string{"+log-list", "--app-id", "app_x", "--as", "user"}, factory, stdout); err != nil {
|
|
t.Fatalf("execute err=%v", err)
|
|
}
|
|
|
|
var env struct {
|
|
Data struct {
|
|
Items []map[string]interface{} `json:"items"`
|
|
} `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(stdout.Bytes(), &env); err != nil {
|
|
t.Fatalf("decode output: %v\n%s", err, stdout.String())
|
|
}
|
|
attrs, ok := env.Data.Items[0]["attributes"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("attributes = %#v, want object", env.Data.Items[0]["attributes"])
|
|
}
|
|
if attrs["app_env"] != "runtime" || attrs["duration_ms"] != "8263" || attrs["module"] != "gateway" {
|
|
t.Fatalf("attributes = %#v", attrs)
|
|
}
|
|
}
|
|
|
|
func TestAppsLogGet_PrettyFormatsTimestamp(t *testing.T) {
|
|
const rawNS = int64(1782209472123456789)
|
|
factory, stdout, reg := newAppsExecuteFactory(t)
|
|
reg.Register(&httpmock.Stub{
|
|
Method: "POST",
|
|
URL: "/open-apis/spark/v1/apps/app_x/search_logs",
|
|
Body: map[string]interface{}{
|
|
"code": 0,
|
|
"data": map[string]interface{}{
|
|
"log_items": []interface{}{
|
|
map[string]interface{}{
|
|
"log_id": "LOG1",
|
|
"level": "ERROR",
|
|
"trace_id": "trace-1",
|
|
"timestamp_ns": rawNS,
|
|
"message": "boom",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
if err := runAppsShortcut(t, AppsLogGet, []string{
|
|
"+log-get", "--app-id", "app_x", "--log-id", "LOG1", "--format", "pretty", "--as", "user",
|
|
}, factory, stdout); err != nil {
|
|
t.Fatalf("execute err=%v", err)
|
|
}
|
|
got := stdout.String()
|
|
wantTime := time.Unix(0, rawNS).Local().Format("2006-01-02 15:04:05.000")
|
|
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_ns") || strings.Contains(got, "1782209472123456789") {
|
|
t.Fatalf("pretty output should hide raw timestamp_ns, got:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestAppsLogGet_ResolvesSourceStackWhenFieldsPresent(t *testing.T) {
|
|
factory, stdout, reg := newAppsExecuteFactory(t)
|
|
search := &httpmock.Stub{
|
|
Method: "POST",
|
|
URL: "/open-apis/spark/v1/apps/app_x/search_logs",
|
|
Body: map[string]interface{}{
|
|
"code": 0,
|
|
"data": map[string]interface{}{
|
|
"log_items": []interface{}{
|
|
map[string]interface{}{
|
|
"log_id": "LOG1",
|
|
"level": "ERROR",
|
|
"attributes": map[string]interface{}{
|
|
"commit_id": "commit_1",
|
|
"source_map_file_prefix": "sourcemaps/app",
|
|
"frames": []interface{}{
|
|
map[string]interface{}{"file": "main.js", "line": 10, "column": 20},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
resolve := &httpmock.Stub{
|
|
Method: "POST",
|
|
URL: "/open-apis/spark/v1/apps/app_x/resolve_stack_trace",
|
|
Body: map[string]interface{}{
|
|
"code": 0,
|
|
"data": map[string]interface{}{
|
|
"source_stack": []interface{}{
|
|
map[string]interface{}{"file": "src/App.tsx", "line": 7, "column": 9},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
reg.Register(search)
|
|
reg.Register(resolve)
|
|
|
|
if err := runAppsShortcut(t, AppsLogGet, []string{"+log-get", "--app-id", "app_x", "--log-id", "LOG1", "--as", "user"}, factory, stdout); err != nil {
|
|
t.Fatalf("execute err=%v", err)
|
|
}
|
|
var sent map[string]interface{}
|
|
if err := json.Unmarshal(resolve.CapturedBody, &sent); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if sent["commit_id"] != "commit_1" || sent["source_map_file_prefix"] != "sourcemaps/app" {
|
|
t.Fatalf("resolve body missing source map fields: %#v", sent)
|
|
}
|
|
frames, ok := sent["frames"].([]interface{})
|
|
if !ok || len(frames) != 1 {
|
|
t.Fatalf("resolve frames = %#v", sent["frames"])
|
|
}
|
|
if got := stdout.String(); !strings.Contains(got, `"source_stack_status": "resolved"`) || !strings.Contains(got, "src/App.tsx") {
|
|
t.Fatalf("stdout missing resolved source stack: %s", got)
|
|
}
|
|
}
|
|
|
|
func TestAppsLogGet_ResolvesSourceStackFromNestedKVAttributes(t *testing.T) {
|
|
factory, stdout, reg := newAppsExecuteFactory(t)
|
|
search := &httpmock.Stub{
|
|
Method: "POST",
|
|
URL: "/open-apis/spark/v1/apps/app_x/search_logs",
|
|
Body: map[string]interface{}{
|
|
"code": 0,
|
|
"data": map[string]interface{}{
|
|
"log_items": []interface{}{
|
|
map[string]interface{}{
|
|
"log_id": "LOG7655249917057764881",
|
|
"severityText": "ERROR",
|
|
"attributes": []interface{}{
|
|
map[string]interface{}{"key": "commit_id", "value": "commit_nested"},
|
|
map[string]interface{}{"key": "source_map_file_prefix", "value": "sourcemaps/nested"},
|
|
map[string]interface{}{
|
|
"key": "exception",
|
|
"value": map[string]interface{}{
|
|
"stackTrace": strings.Join([]string{
|
|
"TypeError: failed to render",
|
|
" at render (https://cdn.example.com/assets/main.js:12:34)",
|
|
" at https://cdn.example.com/assets/chunk.js:56:78",
|
|
}, "\n"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
resolve := &httpmock.Stub{
|
|
Method: "POST",
|
|
URL: "/open-apis/spark/v1/apps/app_x/resolve_stack_trace",
|
|
Body: map[string]interface{}{
|
|
"code": 0,
|
|
"data": map[string]interface{}{
|
|
"source_stack": []interface{}{
|
|
map[string]interface{}{"file": "src/App.tsx", "line": 12, "column": 34},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
reg.Register(search)
|
|
reg.Register(resolve)
|
|
|
|
if err := runAppsShortcut(t, AppsLogGet, []string{"+log-get", "--app-id", "app_x", "--log-id", "LOG7655249917057764881", "--as", "user"}, factory, stdout); err != nil {
|
|
t.Fatalf("execute err=%v", err)
|
|
}
|
|
var sent map[string]interface{}
|
|
if err := json.Unmarshal(resolve.CapturedBody, &sent); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if sent["commit_id"] != "commit_nested" || sent["source_map_file_prefix"] != "sourcemaps/nested" {
|
|
t.Fatalf("resolve body missing nested source map fields: %#v", sent)
|
|
}
|
|
frames, ok := sent["frames"].([]interface{})
|
|
if !ok || len(frames) != 2 {
|
|
t.Fatalf("resolve frames = %#v, want parsed stack frames", sent["frames"])
|
|
}
|
|
frame, ok := frames[0].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("parsed frame = %#v, want object", frames[0])
|
|
}
|
|
if frame["function"] != "render" || frame["file_name"] != "main.js" || frame["line"] != float64(12) || frame["column"] != float64(34) {
|
|
t.Fatalf("parsed frame = %#v", frame)
|
|
}
|
|
bare, ok := frames[1].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("bare frame = %#v, want object", frames[1])
|
|
}
|
|
if bare["file_name"] != "chunk.js" || bare["line"] != float64(56) || bare["column"] != float64(78) {
|
|
t.Fatalf("bare frame = %#v", bare)
|
|
}
|
|
if got := stdout.String(); !strings.Contains(got, `"source_stack_status": "resolved"`) || !strings.Contains(got, "src/App.tsx") {
|
|
t.Fatalf("stdout missing resolved source stack: %s", got)
|
|
}
|
|
}
|
|
|
|
func TestAppsLogGet_ResolvesSourceStackFromReleaseCommitJSONStack(t *testing.T) {
|
|
factory, stdout, reg := newAppsExecuteFactory(t)
|
|
search := &httpmock.Stub{
|
|
Method: "POST",
|
|
URL: "/open-apis/spark/v1/apps/app_x/search_logs",
|
|
Body: map[string]interface{}{
|
|
"code": 0,
|
|
"data": map[string]interface{}{
|
|
"log_items": []interface{}{
|
|
map[string]interface{}{
|
|
"log_id": "LOG7655249917057764881",
|
|
"severityText": "ERROR",
|
|
"attributes": map[string]interface{}{
|
|
"tenant_id": "110564",
|
|
"release_commit_id": "4b393e4e0ca9ca1a855ba4585bc6750a7db2266f",
|
|
"stack": `[{"fileName":"main.js","line":3348,"column":540585},` +
|
|
`{"fileName":"main.js","line":3107,"column":51935},` +
|
|
`{"fileName":"main.js","line":62,"column":12516}]`,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
resolve := &httpmock.Stub{
|
|
Method: "POST",
|
|
URL: "/open-apis/spark/v1/apps/app_x/resolve_stack_trace",
|
|
Body: map[string]interface{}{
|
|
"code": 0,
|
|
"data": map[string]interface{}{
|
|
"source_stack": []interface{}{
|
|
map[string]interface{}{"file": "src/App.tsx", "line": 42, "column": 7},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
reg.Register(search)
|
|
reg.Register(resolve)
|
|
|
|
if err := runAppsShortcut(t, AppsLogGet, []string{"+log-get", "--app-id", "app_x", "--log-id", "LOG7655249917057764881", "--as", "user"}, factory, stdout); err != nil {
|
|
t.Fatalf("execute err=%v", err)
|
|
}
|
|
var sent map[string]interface{}
|
|
if err := json.Unmarshal(resolve.CapturedBody, &sent); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if sent["commit_id"] != "4b393e4e0ca9ca1a855ba4585bc6750a7db2266f" || sent["source_map_file_prefix"] != defaultSourceMapPrefix || sent["tenant_id"] != "110564" {
|
|
t.Fatalf("resolve body missing release source map fields: %#v", sent)
|
|
}
|
|
frames, ok := sent["frames"].([]interface{})
|
|
if !ok || len(frames) != 3 {
|
|
t.Fatalf("resolve frames = %#v, want all valid generated frames", sent["frames"])
|
|
}
|
|
first, ok := frames[0].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("first frame = %#v, want object", frames[0])
|
|
}
|
|
if first["file_name"] != "main.js" || first["line"] != float64(3348) || first["column"] != float64(540585) {
|
|
t.Fatalf("first frame = %#v", first)
|
|
}
|
|
if got := stdout.String(); !strings.Contains(got, `"source_stack_status": "resolved"`) || !strings.Contains(got, "src/App.tsx") {
|
|
t.Fatalf("stdout missing resolved source stack: %s", got)
|
|
}
|
|
}
|
|
|
|
func TestAppsLogGet_ResolvesSourceStackFromJSONBodyStack(t *testing.T) {
|
|
factory, stdout, reg := newAppsExecuteFactory(t)
|
|
search := &httpmock.Stub{
|
|
Method: "POST",
|
|
URL: "/open-apis/spark/v1/apps/app_x/search_logs",
|
|
Body: map[string]interface{}{
|
|
"code": 0,
|
|
"data": map[string]interface{}{
|
|
"log_items": []interface{}{
|
|
map[string]interface{}{
|
|
"log_id": "LOG_BODY_STACK",
|
|
"severityText": "ERROR",
|
|
"attributes": map[string]interface{}{
|
|
"release_commit_id": "commit_body",
|
|
},
|
|
"body": `{"error":{"stack":"AxiosError: failed\n at request (https://cdn.example.com/client/assets/body.js:9:88)"}}`,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
resolve := &httpmock.Stub{
|
|
Method: "POST",
|
|
URL: "/open-apis/spark/v1/apps/app_x/resolve_stack_trace",
|
|
Body: map[string]interface{}{
|
|
"code": 0,
|
|
"data": map[string]interface{}{
|
|
"source_stack": []interface{}{
|
|
map[string]interface{}{"file": "src/request.ts", "line": 9, "column": 88},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
reg.Register(search)
|
|
reg.Register(resolve)
|
|
|
|
if err := runAppsShortcut(t, AppsLogGet, []string{"+log-get", "--app-id", "app_x", "--log-id", "LOG_BODY_STACK", "--as", "user"}, factory, stdout); err != nil {
|
|
t.Fatalf("execute err=%v", err)
|
|
}
|
|
var sent map[string]interface{}
|
|
if err := json.Unmarshal(resolve.CapturedBody, &sent); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if sent["commit_id"] != "commit_body" || sent["source_map_file_prefix"] != defaultSourceMapPrefix {
|
|
t.Fatalf("resolve body missing body stack source map fields: %#v", sent)
|
|
}
|
|
frames, ok := sent["frames"].([]interface{})
|
|
if !ok || len(frames) != 1 {
|
|
t.Fatalf("resolve frames = %#v, want parsed JSON body stack frame", sent["frames"])
|
|
}
|
|
frame, ok := frames[0].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("frame = %#v, want object", frames[0])
|
|
}
|
|
if frame["function"] != "request" || frame["file_name"] != "body.js" || frame["line"] != float64(9) || frame["column"] != float64(88) {
|
|
t.Fatalf("frame = %#v", frame)
|
|
}
|
|
if got := stdout.String(); !strings.Contains(got, `"source_stack_status": "resolved"`) || !strings.Contains(got, "src/request.ts") {
|
|
t.Fatalf("stdout missing resolved source stack: %s", got)
|
|
}
|
|
}
|
|
|
|
func TestAppsLogGet_SourceStackMissingFieldsDoesNotFail(t *testing.T) {
|
|
factory, stdout, reg := newAppsExecuteFactory(t)
|
|
search := &httpmock.Stub{
|
|
Method: "POST",
|
|
URL: "/open-apis/spark/v1/apps/app_x/search_logs",
|
|
Body: map[string]interface{}{
|
|
"code": 0,
|
|
"data": map[string]interface{}{
|
|
"log_items": []interface{}{
|
|
map[string]interface{}{
|
|
"log_id": "LOG1",
|
|
"level": "ERROR",
|
|
"message": "TypeError at https://cdn.example.com/main.js:10:20",
|
|
"attributes": map[string]interface{}{"commit_id": "commit_1"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
reg.Register(search)
|
|
|
|
if err := runAppsShortcut(t, AppsLogGet, []string{"+log-get", "--app-id", "app_x", "--log-id", "LOG1", "--as", "user"}, factory, stdout); err != nil {
|
|
t.Fatalf("execute err=%v", err)
|
|
}
|
|
if got := stdout.String(); !strings.Contains(got, `"log_id": "LOG1"`) {
|
|
t.Fatalf("stdout missing original log: %s", got)
|
|
} else if !strings.Contains(got, `"source_stack_status": "unresolved"`) {
|
|
t.Fatalf("stdout missing unresolved source stack status: %s", got)
|
|
} else if !strings.Contains(got, `"source_stack_reason"`) {
|
|
t.Fatalf("stdout missing sanitized source stack reason: %s", got)
|
|
}
|
|
for _, banned := range []string{"secret", "token", "raw request payload"} {
|
|
if strings.Contains(strings.ToLower(stdout.String()), banned) {
|
|
t.Fatalf("stdout leaked %q: %s", banned, stdout.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAppsLogGet_ErrorNonFrontendMissingFieldsDoesNotMarkUnresolved(t *testing.T) {
|
|
factory, stdout, reg := newAppsExecuteFactory(t)
|
|
reg.Register(&httpmock.Stub{
|
|
Method: "POST",
|
|
URL: "/open-apis/spark/v1/apps/app_x/search_logs",
|
|
Body: map[string]interface{}{
|
|
"code": 0,
|
|
"data": map[string]interface{}{
|
|
"log_items": []interface{}{
|
|
map[string]interface{}{
|
|
"log_id": "LOG1",
|
|
"level": "ERROR",
|
|
"message": "go stack trace: database query failed",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
if err := runAppsShortcut(t, AppsLogGet, []string{"+log-get", "--app-id", "app_x", "--log-id", "LOG1", "--as", "user"}, factory, stdout); err != nil {
|
|
t.Fatalf("execute err=%v", err)
|
|
}
|
|
if got := stdout.String(); strings.Contains(got, "source_stack_status") {
|
|
t.Fatalf("non-frontend error log should not be marked unresolved: %s", got)
|
|
}
|
|
}
|
|
|
|
func TestAppsLogGet_SourceStackResolveFailureIsRedacted(t *testing.T) {
|
|
factory, stdout, reg := newAppsExecuteFactory(t)
|
|
search := &httpmock.Stub{
|
|
Method: "POST",
|
|
URL: "/open-apis/spark/v1/apps/app_x/search_logs",
|
|
Body: map[string]interface{}{
|
|
"code": 0,
|
|
"data": map[string]interface{}{
|
|
"log_items": []interface{}{
|
|
map[string]interface{}{
|
|
"log_id": "LOG1",
|
|
"level": "ERROR",
|
|
"attributes": map[string]interface{}{
|
|
"commit_id": "commit_1",
|
|
"source_map_file_prefix": "sourcemaps/app",
|
|
"frames": []interface{}{
|
|
map[string]interface{}{"file": "main.js", "line": 10, "column": 20},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
resolve := &httpmock.Stub{
|
|
Method: "POST",
|
|
URL: "/open-apis/spark/v1/apps/app_x/resolve_stack_trace",
|
|
Body: map[string]interface{}{
|
|
"code": 999,
|
|
"msg": "secret token raw request payload should be redacted",
|
|
},
|
|
}
|
|
reg.Register(search)
|
|
reg.Register(resolve)
|
|
|
|
if err := runAppsShortcut(t, AppsLogGet, []string{"+log-get", "--app-id", "app_x", "--log-id", "LOG1", "--as", "user"}, factory, stdout); err != nil {
|
|
t.Fatalf("execute err=%v", err)
|
|
}
|
|
got := stdout.String()
|
|
if !strings.Contains(got, `"source_stack_status": "unresolved"`) {
|
|
t.Fatalf("stdout missing unresolved status: %s", got)
|
|
}
|
|
if !strings.Contains(got, `"source_stack_error_code": 999`) {
|
|
t.Fatalf("stdout missing resolve error code: %s", got)
|
|
}
|
|
for _, banned := range []string{"secret", "token", "raw request payload"} {
|
|
if strings.Contains(strings.ToLower(got), banned) {
|
|
t.Fatalf("stdout leaked %q: %s", banned, got)
|
|
}
|
|
}
|
|
}
|