mirror of
https://github.com/larksuite/cli.git
synced 2026-07-08 18:13:01 +08:00
feat(apps): add queryAppMeta shared function for app_type/arch_type lookup
This commit is contained in:
65
shortcuts/apps/apps_meta.go
Normal file
65
shortcuts/apps/apps_meta.go
Normal file
@@ -0,0 +1,65 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package apps
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/larksuite/cli/internal/validate"
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
)
|
||||
|
||||
// appMeta holds the numeric app_type and arch_type returned by the server.
|
||||
type appMeta struct {
|
||||
AppType int
|
||||
ArchType int
|
||||
}
|
||||
|
||||
// queryAppMeta fetches the app's metadata (numeric app_type and arch_type)
|
||||
// from the server. Returns nil when the API is unavailable or returns an
|
||||
// error — callers fall back to legacy behavior.
|
||||
func queryAppMeta(ctx context.Context, rctx *common.RuntimeContext, appID string) *appMeta {
|
||||
path := fmt.Sprintf("%s/apps/%s", apiBasePath, validate.EncodePathSegment(appID))
|
||||
data, err := rctx.CallAPITyped("GET", path, nil, nil)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
app, _ := data["app"].(map[string]interface{})
|
||||
if app == nil {
|
||||
return nil
|
||||
}
|
||||
appType, ok1 := toInt(app["app_type"])
|
||||
archType, ok2 := toInt(app["arch_type"])
|
||||
if !ok1 || !ok2 {
|
||||
return nil
|
||||
}
|
||||
return &appMeta{AppType: appType, ArchType: archType}
|
||||
}
|
||||
|
||||
// toInt converts a JSON number (float64 or json.Number) to int.
|
||||
func toInt(v interface{}) (int, bool) {
|
||||
switch n := v.(type) {
|
||||
case float64:
|
||||
return int(n), true
|
||||
case int:
|
||||
return n, true
|
||||
case json.Number:
|
||||
i, err := strconv.Atoi(n.String())
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
return i, true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
// isStaticHtml reports whether the app is a legacy static-HTML app
|
||||
// (app_type=7, arch_type=3) that has no template and no env vars.
|
||||
func isStaticHtml(meta *appMeta) bool {
|
||||
return meta != nil && meta.AppType == 7 && meta.ArchType == 3
|
||||
}
|
||||
160
shortcuts/apps/apps_meta_test.go
Normal file
160
shortcuts/apps/apps_meta_test.go
Normal file
@@ -0,0 +1,160 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package apps
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
"github.com/larksuite/cli/internal/httpmock"
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
)
|
||||
|
||||
// newMetaTestRuntime creates a RuntimeContext wired to an httpmock.Registry
|
||||
// so queryAppMeta can be tested without a real server.
|
||||
func newMetaTestRuntime(t *testing.T) (*common.RuntimeContext, *httpmock.Registry) {
|
||||
t.Helper()
|
||||
cfg := &core.CliConfig{Brand: core.BrandFeishu, AppID: "cli_meta_test"}
|
||||
f, _, _, reg := cmdutil.TestFactory(t, cfg)
|
||||
rt := common.TestNewRuntimeContextForAPI(
|
||||
context.Background(),
|
||||
&cobra.Command{Use: "+meta-test"},
|
||||
cfg, f, core.AsUser,
|
||||
)
|
||||
return rt, reg
|
||||
}
|
||||
|
||||
func TestQueryAppMeta_Success(t *testing.T) {
|
||||
rt, reg := newMetaTestRuntime(t)
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "GET",
|
||||
URL: "/open-apis/spark/v1/apps/app_test123",
|
||||
Body: map[string]interface{}{
|
||||
"code": float64(0),
|
||||
"data": map[string]interface{}{
|
||||
"app": map[string]interface{}{
|
||||
"app_type": float64(7),
|
||||
"arch_type": float64(4),
|
||||
"name": "Test App",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
meta := queryAppMeta(context.Background(), rt, "app_test123")
|
||||
if meta == nil {
|
||||
t.Fatal("expected non-nil appMeta")
|
||||
}
|
||||
if meta.AppType != 7 {
|
||||
t.Errorf("AppType = %d, want 7", meta.AppType)
|
||||
}
|
||||
if meta.ArchType != 4 {
|
||||
t.Errorf("ArchType = %d, want 4", meta.ArchType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryAppMeta_APIError(t *testing.T) {
|
||||
rt, reg := newMetaTestRuntime(t)
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "GET",
|
||||
URL: "/open-apis/spark/v1/apps/app_err",
|
||||
Body: map[string]interface{}{
|
||||
"code": float64(500100),
|
||||
"msg": "internal server error",
|
||||
},
|
||||
})
|
||||
|
||||
meta := queryAppMeta(context.Background(), rt, "app_err")
|
||||
if meta != nil {
|
||||
t.Fatalf("expected nil for API error, got %+v", meta)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryAppMeta_MissingFields(t *testing.T) {
|
||||
rt, reg := newMetaTestRuntime(t)
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "GET",
|
||||
URL: "/open-apis/spark/v1/apps/app_partial",
|
||||
Body: map[string]interface{}{
|
||||
"code": float64(0),
|
||||
"data": map[string]interface{}{
|
||||
"app": map[string]interface{}{
|
||||
"name": "Partial App",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
meta := queryAppMeta(context.Background(), rt, "app_partial")
|
||||
if meta != nil {
|
||||
t.Fatalf("expected nil when app_type/arch_type are missing, got %+v", meta)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryAppMeta_NoAppObject(t *testing.T) {
|
||||
rt, reg := newMetaTestRuntime(t)
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "GET",
|
||||
URL: "/open-apis/spark/v1/apps/app_noobj",
|
||||
Body: map[string]interface{}{
|
||||
"code": float64(0),
|
||||
"data": map[string]interface{}{},
|
||||
},
|
||||
})
|
||||
|
||||
meta := queryAppMeta(context.Background(), rt, "app_noobj")
|
||||
if meta != nil {
|
||||
t.Fatalf("expected nil when app object is absent, got %+v", meta)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsStaticHtml(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
meta *appMeta
|
||||
want bool
|
||||
}{
|
||||
{"nil meta", nil, false},
|
||||
{"static html (7,3)", &appMeta{AppType: 7, ArchType: 3}, true},
|
||||
{"full stack (7,4)", &appMeta{AppType: 7, ArchType: 4}, false},
|
||||
{"other type (3,0)", &appMeta{AppType: 3, ArchType: 0}, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := isStaticHtml(tt.meta); got != tt.want {
|
||||
t.Errorf("isStaticHtml(%+v) = %v, want %v", tt.meta, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToInt(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input interface{}
|
||||
wantN int
|
||||
wantOK bool
|
||||
}{
|
||||
{"float64", float64(42), 42, true},
|
||||
{"int", 7, 7, true},
|
||||
{"json.Number", json.Number("99"), 99, true},
|
||||
{"json.Number non-int", json.Number("abc"), 0, false},
|
||||
{"string", "7", 0, false},
|
||||
{"nil", nil, 0, false},
|
||||
{"bool", true, 0, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
n, ok := toInt(tt.input)
|
||||
if n != tt.wantN || ok != tt.wantOK {
|
||||
t.Errorf("toInt(%v) = (%d, %v), want (%d, %v)", tt.input, n, ok, tt.wantN, tt.wantOK)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user