项目文件夹

文件
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

317 行
14 KiB
Go

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package apps
import (
"encoding/json"
"errors"
"net/http"
"strings"
"testing"
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/httpmock"
)
const (
dbAuditStatusURL = "/open-apis/spark/v1/apps/app_x/db/audit_status"
dbAuditSetURL = "/open-apis/spark/v1/apps/app_x/db/audit_set"
dbAuditListURL = "/open-apis/spark/v1/apps/app_x/db/audit_list"
dbTablesListURL = "/open-apis/spark/v1/apps/app_x/tables"
)
// ── audit-status ──
// TestAppsDBAuditStatus_SingleTableObjectWithPlaceholder 验证单表查询无记录时返回 enabled:false 的占位对象(非数组)。
func TestAppsDBAuditStatus_SingleTableObjectWithPlaceholder(t *testing.T) {
factory, stdout, reg := newAppsExecuteFactory(t)
reg.Register(&httpmock.Stub{
Method: "GET", URL: dbAuditStatusURL,
Body: map[string]interface{}{"code": 0, "data": map[string]interface{}{"items": []interface{}{}}},
})
if err := runAppsShortcut(t, AppsDBAuditStatus,
[]string{"+db-audit-status", "--app-id", "app_x", "--table", "orders", "--as", "user"}, factory, stdout); err != nil {
t.Fatalf("execute err=%v", err)
}
// 单表无记录 → 占位对象 enabled:false(不是数组)。
var env struct {
Data struct {
Table string `json:"table"`
Enabled bool `json:"enabled"`
} `json:"data"`
}
if err := json.Unmarshal([]byte(stdout.String()), &env); err != nil {
t.Fatalf("decode: %v\n%s", err, stdout.String())
}
if env.Data.Table != "orders" || env.Data.Enabled {
t.Fatalf("expected placeholder {orders,false}, got %+v", env.Data)
}
}
// TestAppsDBAuditStatus_MultiTablePrettyTable 验证多表 pretty 输出含 enabled/yes/no 列与 retention 值。
func TestAppsDBAuditStatus_MultiTablePrettyTable(t *testing.T) {
factory, stdout, reg := newAppsExecuteFactory(t)
reg.Register(&httpmock.Stub{
Method: "GET", URL: dbAuditStatusURL,
Body: map[string]interface{}{"code": 0, "data": map[string]interface{}{"items": []interface{}{
map[string]interface{}{"table": "orders", "enabled": true, "enabled_at": "2026-04-15T10:30:00Z", "retention": "30d"},
map[string]interface{}{"table": "users", "enabled": false},
}}},
})
if err := runAppsShortcut(t, AppsDBAuditStatus,
[]string{"+db-audit-status", "--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, "enabled") || !strings.Contains(got, "yes") || !strings.Contains(got, "no") || !strings.Contains(got, "30d") {
t.Fatalf("pretty table malformed:\n%s", got)
}
}
// ── audit-enable / disable ──
// TestAppsDBAuditEnable_RequiresTableAndValidRetention 验证缺 --table 报必填错、非法 --retention 报 ValidationError。
func TestAppsDBAuditEnable_RequiresTableAndValidRetention(t *testing.T) {
factory, stdout, _ := newAppsExecuteFactory(t)
// 缺 --table → cobra required, exit 1
if err := runAppsShortcut(t, AppsDBAuditEnable,
[]string{"+db-audit-enable", "--app-id", "app_x", "--as", "user"}, factory, stdout); err == nil {
t.Fatalf("expected required --table error")
}
// 非法 retention → enum 校验 (validation)
factory2, stdout2, _ := newAppsExecuteFactory(t)
err := runAppsShortcut(t, AppsDBAuditEnable,
[]string{"+db-audit-enable", "--app-id", "app_x", "--table", "orders", "--retention", "99d", "--as", "user"}, factory2, stdout2)
var ve *errs.ValidationError
if !errors.As(err, &ve) {
t.Fatalf("err = %T %v, want *errs.ValidationError", err, err)
}
if ve.Param != "--retention" {
t.Fatalf("Param = %q, want --retention", ve.Param)
}
}
// TestAppsDBAuditEnable_DryRunAndSuccess 验证 dry-run 发出 enabled:true+retention 的 POST,成功时打印 pretty 确认行。
func TestAppsDBAuditEnable_DryRunAndSuccess(t *testing.T) {
// dry-run body {table, enabled:true, retention}
factory, stdout, _ := newAppsExecuteFactory(t)
if err := runAppsShortcut(t, AppsDBAuditEnable,
[]string{"+db-audit-enable", "--app-id", "app_x", "--table", "orders", "--retention", "30d", "--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"`
Body map[string]interface{} `json:"body"`
} `json:"api"`
}
_ = json.Unmarshal([]byte(stdout.String()), &env)
a := env.API[0]
if a.Method != "POST" || a.URL != dbAuditSetURL || a.Body["enabled"] != true || a.Body["retention"] != "30d" || a.Body["table"] != "orders" {
t.Fatalf("dry-run = %s %s body=%v", a.Method, a.URL, a.Body)
}
// success
factory2, stdout2, reg := newAppsExecuteFactory(t)
reg.Register(&httpmock.Stub{
Method: "POST", URL: dbAuditSetURL,
Body: map[string]interface{}{"code": 0, "data": map[string]interface{}{"status": map[string]interface{}{"table": "orders", "enabled": true, "retention": "30d"}}},
})
if err := runAppsShortcut(t, AppsDBAuditEnable,
[]string{"+db-audit-enable", "--app-id", "app_x", "--table", "orders", "--retention", "30d", "--format", "pretty", "--as", "user"}, factory2, stdout2); err != nil {
t.Fatalf("execute err=%v", err)
}
if !strings.Contains(stdout2.String(), "✓ Audit enabled for table 'orders' (retention: 30d)") {
t.Fatalf("pretty: %s", stdout2.String())
}
}
// TestAppsDBAuditDisable_DryRunAndSuccess 验证 dry-run 发出 enabled:false 的 POST,成功时打印 pretty 确认行。
func TestAppsDBAuditDisable_DryRunAndSuccess(t *testing.T) {
factory, stdout, _ := newAppsExecuteFactory(t)
if err := runAppsShortcut(t, AppsDBAuditDisable,
[]string{"+db-audit-disable", "--app-id", "app_x", "--table", "orders", "--dry-run", "--as", "user"}, factory, stdout); err != nil {
t.Fatalf("dry-run err=%v", err)
}
var env struct {
API []struct {
Body map[string]interface{} `json:"body"`
} `json:"api"`
}
_ = json.Unmarshal([]byte(stdout.String()), &env)
if env.API[0].Body["enabled"] != false || env.API[0].Body["table"] != "orders" {
t.Fatalf("dry-run body=%v (want enabled:false)", env.API[0].Body)
}
factory2, stdout2, reg := newAppsExecuteFactory(t)
reg.Register(&httpmock.Stub{
Method: "POST", URL: dbAuditSetURL,
Body: map[string]interface{}{"code": 0, "data": map[string]interface{}{"status": map[string]interface{}{"table": "orders", "enabled": false}}},
})
if err := runAppsShortcut(t, AppsDBAuditDisable,
[]string{"+db-audit-disable", "--app-id", "app_x", "--table", "orders", "--format", "pretty", "--as", "user"}, factory2, stdout2); err != nil {
t.Fatalf("execute err=%v", err)
}
if !strings.Contains(stdout2.String(), "✓ Audit disabled for table 'orders'") {
t.Fatalf("pretty: %s", stdout2.String())
}
}
// ── audit-list ──
// TestAppsDBAuditList_RequiresTable 验证缺 --table 时报必填错误。
func TestAppsDBAuditList_RequiresTable(t *testing.T) {
factory, stdout, _ := newAppsExecuteFactory(t)
if err := runAppsShortcut(t, AppsDBAuditList,
[]string{"+db-audit-list", "--app-id", "app_x", "--as", "user"}, factory, stdout); err == nil {
t.Fatalf("expected required --table error")
}
}
// TestAppsDBAuditList_DryRunJoinsTables 验证 dry-run 将多个 --table 合并为 tables=orders,users 且归一化 since。
func TestAppsDBAuditList_DryRunJoinsTables(t *testing.T) {
factory, stdout, _ := newAppsExecuteFactory(t)
if err := runAppsShortcut(t, AppsDBAuditList,
[]string{"+db-audit-list", "--app-id", "app_x", "--table", "orders", "--table", "users", "--since", "7d", "--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"`
}
_ = json.Unmarshal([]byte(stdout.String()), &env)
a := env.API[0]
if a.Method != "GET" || a.URL != dbAuditListURL || a.Params["tables"] != "orders,users" {
t.Fatalf("dry-run = %s %s tables=%v", a.Method, a.URL, a.Params["tables"])
}
if s, _ := a.Params["since"].(string); !strings.HasSuffix(s, "Z") {
t.Fatalf("since not normalized: %v", a.Params["since"])
}
}
// 单表查询:不预过滤、直接打 audit_list(后端就 not-found/not-enabled 报错),无 skipped。
// TestAppsDBAuditList_SingleTableNoPreflight 验证单表查询不预过滤、operator/before/after 还原为对象、无 skipped。
func TestAppsDBAuditList_SingleTableNoPreflight(t *testing.T) {
factory, stdout, reg := newAppsExecuteFactory(t)
reg.Register(&httpmock.Stub{
Method: "GET", URL: dbAuditListURL,
Body: map[string]interface{}{"code": 0, "data": map[string]interface{}{
"has_more": false, "page_token": "",
"items": []interface{}{map[string]interface{}{
"event_id": "01525", "event_time": "2026-04-16T10:30:00Z", "target_table": "users",
"type": "UPDATE", "operator": `{"id":"7311","name":"alice"}`, "summary": "UPDATE 1 field",
"before": `{"amount":100}`, "after": `{"amount":999}`,
}},
}},
})
if err := runAppsShortcut(t, AppsDBAuditList,
[]string{"+db-audit-list", "--app-id", "app_x", "--table", "users", "--as", "user"}, factory, stdout); err != nil {
t.Fatalf("execute err=%v", err)
}
got := stdout.String()
// operator → 对象;before/after → 还原成对象(非字符串)。
for _, want := range []string{`"name": "alice"`, `"before"`, `"amount": 100`, `"after"`, `"amount": 999`} {
if !strings.Contains(got, want) {
t.Errorf("missing %q:\n%s", want, got)
}
}
if strings.Contains(got, `"skipped"`) {
t.Errorf("single-table query must not emit skipped:\n%s", got)
}
if strings.Contains(got, `"before": "{`) {
t.Errorf("before should be an object, not a JSON string:\n%s", got)
}
}
// TestAppsDBAuditList_SingleTableEmptyPretty 验证单表无事件时不报错、pretty 打印 "No audit events found." 且无 Skipped。
func TestAppsDBAuditList_SingleTableEmptyPretty(t *testing.T) {
factory, stdout, reg := newAppsExecuteFactory(t)
reg.Register(&httpmock.Stub{
Method: "GET", URL: dbAuditListURL,
Body: map[string]interface{}{"code": 0, "data": map[string]interface{}{"items": []interface{}{}}},
})
if err := runAppsShortcut(t, AppsDBAuditList,
[]string{"+db-audit-list", "--app-id", "app_x", "--table", "orders", "--format", "pretty", "--as", "user"}, factory, stdout); err != nil {
t.Fatalf("empty audit list should NOT error (ok read), got %v", err)
}
got := stdout.String()
if !strings.Contains(got, "No audit events found.") || strings.Contains(got, "Skipped") {
t.Fatalf("expected empty, no skipped for single table:\n%s", got)
}
}
// 多表查询:CLI 用 schema(存在性)+ status(审计开关)预过滤,只把有效表传给 audit_list,
// 不存在 / 未开启审计的表进 skipped。
// TestAppsDBAuditList_MultiTablePreflightFilters 验证多表查询用 schema+status 预过滤,仅传有效表,不存在/未开审计的表进 skipped。
func TestAppsDBAuditList_MultiTablePreflightFilters(t *testing.T) {
factory, stdout, reg := newAppsExecuteFactory(t)
// schemaorders/users/carts 存在,ghost 不存在。
reg.Register(&httpmock.Stub{
Method: "GET", URL: dbTablesListURL,
Body: map[string]interface{}{"code": 0, "data": map[string]interface{}{"has_more": false, "items": []interface{}{
map[string]interface{}{"name": "orders"}, map[string]interface{}{"name": "users"}, map[string]interface{}{"name": "carts"},
}}},
})
// statusorders/users 开启审计,carts 未开启。
reg.Register(&httpmock.Stub{
Method: "GET", URL: dbAuditStatusURL,
Body: map[string]interface{}{"code": 0, "data": map[string]interface{}{"items": []interface{}{
map[string]interface{}{"table": "orders", "enabled": true}, map[string]interface{}{"table": "users", "enabled": true},
map[string]interface{}{"table": "carts", "enabled": false},
}}},
})
// audit_list 只应被传入有效表 orders,users。
reg.Register(&httpmock.Stub{
Method: "GET", URL: dbAuditListURL,
OnMatch: func(req *http.Request) {
if got := req.URL.Query().Get("tables"); got != "orders,users" {
t.Errorf("audit_list tables = %q, want orders,users (filtered)", got)
}
},
Body: map[string]interface{}{"code": 0, "data": map[string]interface{}{"has_more": false, "items": []interface{}{
map[string]interface{}{"event_id": "e1", "event_time": "2026-04-16T10:30:00Z", "target_table": "orders", "type": "INSERT", "summary": "INSERT"},
}}},
})
if err := runAppsShortcut(t, AppsDBAuditList,
[]string{"+db-audit-list", "--app-id", "app_x", "--table", "orders", "--table", "users", "--table", "carts", "--table", "ghost", "--as", "user"}, factory, stdout); err != nil {
t.Fatalf("execute err=%v", err)
}
got := stdout.String()
// skippedcarts(audit not enabled) + ghost(table not found),结构化 {table,reason}。
for _, want := range []string{`"skipped"`, `"table": "carts"`, `"reason": "audit not enabled"`, `"table": "ghost"`, `"reason": "table not found"`} {
if !strings.Contains(got, want) {
t.Errorf("missing %q:\n%s", want, got)
}
}
}
// 多表查询且全部被过滤掉 → 不调 audit_list,直接空 + skipped 提示。
// TestAppsDBAuditList_MultiTableAllFilteredSkipsQuery 验证多表全部被过滤时跳过 audit_list 调用,直接输出空结果加 Skipped 提示。
func TestAppsDBAuditList_MultiTableAllFilteredSkipsQuery(t *testing.T) {
factory, stdout, reg := newAppsExecuteFactory(t)
reg.Register(&httpmock.Stub{
Method: "GET", URL: dbTablesListURL,
Body: map[string]interface{}{"code": 0, "data": map[string]interface{}{"has_more": false, "items": []interface{}{
map[string]interface{}{"name": "orders"},
}}},
})
reg.Register(&httpmock.Stub{
Method: "GET", URL: dbAuditStatusURL,
Body: map[string]interface{}{"code": 0, "data": map[string]interface{}{"items": []interface{}{}}},
})
// 不注册 audit_list:若被调用会命中未注册请求而报错。
if err := runAppsShortcut(t, AppsDBAuditList,
[]string{"+db-audit-list", "--app-id", "app_x", "--table", "ghost1", "--table", "ghost2", "--format", "pretty", "--as", "user"}, factory, stdout); err != nil {
t.Fatalf("all-filtered should still succeed (empty), got %v", err)
}
got := stdout.String()
if !strings.Contains(got, "No audit events found.") || !strings.Contains(got, "Skipped 2 of 2 tables") {
t.Fatalf("expected empty + 'Skipped 2 of 2 tables':\n%s", got)
}
}