mirror of
https://github.com/larksuite/cli.git
synced 2026-07-05 07:31:22 +08:00
Adds the apps domain to lark-cli for managing Miaoda (妙搭) applications: 6 shortcuts covering the full lifecycle (+create / +update / +list / +access-scope-set / +access-scope-get / +html-publish). Aligned with the OAPI v2 design — app_type enum (currently HTML), string scope enum (All / Tenant / Range), cursor pagination, in-memory tar.gz multipart publish flow. Namespace registered at /open-apis/spark/v1/ with spark:app.* scopes. --------- Co-authored-by: wangjiangwen-gif <286006750+wangjiangwen-gif@users.noreply.github.com>
204 lines
6.7 KiB
Go
204 lines
6.7 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||
// SPDX-License-Identifier: MIT
|
||
|
||
package apps
|
||
|
||
import (
|
||
"encoding/json"
|
||
"strings"
|
||
"testing"
|
||
|
||
"github.com/larksuite/cli/internal/httpmock"
|
||
)
|
||
|
||
func TestAppsAccessScopeSet_Specific(t *testing.T) {
|
||
factory, stdout, reg := newAppsExecuteFactory(t)
|
||
stub := &httpmock.Stub{
|
||
Method: "PUT",
|
||
URL: "/open-apis/spark/v1/apps/app_x/access-scope",
|
||
Body: map[string]interface{}{"code": 0, "data": map[string]interface{}{}},
|
||
}
|
||
reg.Register(stub)
|
||
|
||
if err := runAppsShortcut(t, AppsAccessScopeSet, []string{
|
||
"+access-scope-set",
|
||
"--app-id", "app_x",
|
||
"--scope", "specific",
|
||
"--targets", `[{"type":"user","id":"ou_xxx"},{"type":"chat","id":"oc_xxx"}]`,
|
||
"--apply-enabled",
|
||
"--approver", "ou_yyy",
|
||
"--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.Fatalf("decode body: %v", err)
|
||
}
|
||
// 新协议:scope 是 string 枚举 (specific=Range),targets 拆成 users/departments/chats
|
||
if got, _ := sent["scope"].(string); got != "Range" {
|
||
t.Fatalf("scope = %v, want %q", sent["scope"], "Range")
|
||
}
|
||
if _, present := sent["targets"]; present {
|
||
t.Fatalf("legacy 'targets' field should not be sent: %v", sent)
|
||
}
|
||
users, _ := sent["users"].([]interface{})
|
||
if len(users) != 1 || users[0] != "ou_xxx" {
|
||
t.Fatalf("users = %v, want [ou_xxx]", sent["users"])
|
||
}
|
||
chats, _ := sent["chats"].([]interface{})
|
||
if len(chats) != 1 || chats[0] != "oc_xxx" {
|
||
t.Fatalf("chats = %v, want [oc_xxx]", sent["chats"])
|
||
}
|
||
if _, present := sent["departments"]; present {
|
||
t.Fatalf("departments should be omitted when empty: %v", sent)
|
||
}
|
||
}
|
||
|
||
func TestAppsAccessScopeSet_Public(t *testing.T) {
|
||
factory, stdout, reg := newAppsExecuteFactory(t)
|
||
reg.Register(&httpmock.Stub{
|
||
Method: "PUT",
|
||
URL: "/open-apis/spark/v1/apps/app_x/access-scope",
|
||
Body: map[string]interface{}{"code": 0, "data": map[string]interface{}{}},
|
||
})
|
||
|
||
if err := runAppsShortcut(t, AppsAccessScopeSet, []string{
|
||
"+access-scope-set",
|
||
"--app-id", "app_x",
|
||
"--scope", "public",
|
||
"--require-login=false",
|
||
"--as", "user",
|
||
}, factory, stdout); err != nil {
|
||
t.Fatalf("execute err=%v", err)
|
||
}
|
||
}
|
||
|
||
func TestAppsAccessScopeSet_Tenant(t *testing.T) {
|
||
factory, stdout, reg := newAppsExecuteFactory(t)
|
||
reg.Register(&httpmock.Stub{
|
||
Method: "PUT",
|
||
URL: "/open-apis/spark/v1/apps/app_x/access-scope",
|
||
Body: map[string]interface{}{"code": 0, "data": map[string]interface{}{}},
|
||
})
|
||
|
||
if err := runAppsShortcut(t, AppsAccessScopeSet, []string{
|
||
"+access-scope-set",
|
||
"--app-id", "app_x",
|
||
"--scope", "tenant",
|
||
"--as", "user",
|
||
}, factory, stdout); err != nil {
|
||
t.Fatalf("execute err=%v", err)
|
||
}
|
||
}
|
||
|
||
func TestAppsAccessScopeSet_SpecificRequiresTargets(t *testing.T) {
|
||
factory, stdout, _ := newAppsExecuteFactory(t)
|
||
err := runAppsShortcut(t, AppsAccessScopeSet, []string{
|
||
"+access-scope-set", "--app-id", "app_x", "--scope", "specific", "--as", "user",
|
||
}, factory, stdout)
|
||
if err == nil || !strings.Contains(err.Error(), "targets") {
|
||
t.Fatalf("expected targets required error, got %v", err)
|
||
}
|
||
}
|
||
|
||
func TestAppsAccessScopeSet_TenantRejectsExtraFlags(t *testing.T) {
|
||
factory, stdout, _ := newAppsExecuteFactory(t)
|
||
err := runAppsShortcut(t, AppsAccessScopeSet, []string{
|
||
"+access-scope-set", "--app-id", "app_x", "--scope", "tenant",
|
||
"--targets", `[]`, "--as", "user",
|
||
}, factory, stdout)
|
||
if err == nil {
|
||
t.Fatalf("expected error when --targets passed with scope=tenant")
|
||
}
|
||
}
|
||
|
||
func TestAppsAccessScopeSet_RejectsBadTargetType(t *testing.T) {
|
||
factory, stdout, _ := newAppsExecuteFactory(t)
|
||
err := runAppsShortcut(t, AppsAccessScopeSet, []string{
|
||
"+access-scope-set", "--app-id", "app_x",
|
||
"--scope", "specific",
|
||
"--targets", `[{"type":"group","id":"oc_xxx"}]`,
|
||
"--as", "user",
|
||
}, factory, stdout)
|
||
if err == nil || !strings.Contains(err.Error(), "type") {
|
||
t.Fatalf("expected bad target type rejected, got %v", err)
|
||
}
|
||
}
|
||
|
||
func TestAppsAccessScopeSet_ApproverRequiresApplyEnabled(t *testing.T) {
|
||
factory, stdout, _ := newAppsExecuteFactory(t)
|
||
err := runAppsShortcut(t, AppsAccessScopeSet, []string{
|
||
"+access-scope-set", "--app-id", "app_x",
|
||
"--scope", "specific",
|
||
"--targets", `[{"type":"user","id":"ou_x"}]`,
|
||
"--approver", "ou_y",
|
||
"--as", "user",
|
||
}, factory, stdout)
|
||
if err == nil || !strings.Contains(err.Error(), "apply-enabled") {
|
||
t.Fatalf("expected --approver requires --apply-enabled, got %v", err)
|
||
}
|
||
}
|
||
|
||
func TestAppsAccessScopeSet_PublicRejectsApprover(t *testing.T) {
|
||
// --approver 只在 specific + apply 流程下有意义;public 模式带它当前会被静默丢弃,
|
||
// 是真实用户语义 bug。这条测试钉死 Validate 阶段拦截。
|
||
factory, stdout, _ := newAppsExecuteFactory(t)
|
||
err := runAppsShortcut(t, AppsAccessScopeSet, []string{
|
||
"+access-scope-set", "--app-id", "app_x",
|
||
"--scope", "public",
|
||
"--require-login=false",
|
||
"--approver", "ou_y",
|
||
"--as", "user",
|
||
}, factory, stdout)
|
||
if err == nil || !strings.Contains(err.Error(), "--approver is not allowed when --scope=public") {
|
||
t.Fatalf("expected --approver rejected for scope=public, got %v", err)
|
||
}
|
||
}
|
||
|
||
func TestAppsAccessScopeSet_PublicRequiresExplicitRequireLogin(t *testing.T) {
|
||
// bare --scope public without --require-login defaults silently to
|
||
// require_login=false (Internet-public + no auth). Reject so the caller
|
||
// has to make an explicit choice; matches SKILL.md "public 必传 --require-login".
|
||
factory, stdout, _ := newAppsExecuteFactory(t)
|
||
err := runAppsShortcut(t, AppsAccessScopeSet, []string{
|
||
"+access-scope-set", "--app-id", "app_x",
|
||
"--scope", "public",
|
||
"--as", "user",
|
||
}, factory, stdout)
|
||
if err == nil || !strings.Contains(err.Error(), "--require-login is required when --scope=public") {
|
||
t.Fatalf("expected --require-login required for public, got %v", err)
|
||
}
|
||
}
|
||
|
||
func TestAppsAccessScopeSet_SpecificRejectsEmptyTargets(t *testing.T) {
|
||
factory, stdout, _ := newAppsExecuteFactory(t)
|
||
err := runAppsShortcut(t, AppsAccessScopeSet, []string{
|
||
"+access-scope-set", "--app-id", "app_x",
|
||
"--scope", "specific",
|
||
"--targets", "[]",
|
||
"--as", "user",
|
||
}, factory, stdout)
|
||
if err == nil || !strings.Contains(err.Error(), "--targets must contain at least one entry") {
|
||
t.Fatalf("expected empty --targets rejected, got %v", err)
|
||
}
|
||
}
|
||
|
||
func TestAppsAccessScopeSet_TrimsAppIDInPath(t *testing.T) {
|
||
factory, stdout, reg := newAppsExecuteFactory(t)
|
||
reg.Register(&httpmock.Stub{
|
||
Method: "PUT",
|
||
URL: "/open-apis/spark/v1/apps/app_x/access-scope",
|
||
Body: map[string]interface{}{"code": 0, "data": map[string]interface{}{}},
|
||
})
|
||
|
||
if err := runAppsShortcut(t, AppsAccessScopeSet, []string{
|
||
"+access-scope-set", "--app-id", " app_x ",
|
||
"--scope", "tenant",
|
||
"--as", "user",
|
||
}, factory, stdout); err != nil {
|
||
t.Fatalf("execute err=%v", err)
|
||
}
|
||
}
|