Files
larksuite-cli/internal/registry/registry_test.go
syh-cpdsss 46468a900c feat: Add whiteboard +query shortcut and enhance +update with Mermaid/PlantUML support (#382)
Change-Id: I719935bb8fee337908ec99d59f1dfaae0df74874
2026-04-10 19:40:29 +08:00

618 lines
17 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package registry
import (
"sort"
"strings"
"testing"
)
func ensureFreshRegistry(t *testing.T) {
t.Helper()
resetInit()
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
t.Setenv("LARKSUITE_CLI_REMOTE_META", "off")
Init()
}
func TestLoadScopePriorities(t *testing.T) {
priorities := LoadScopePriorities()
if len(priorities) == 0 {
t.Fatal("expected non-empty priorities map")
}
t.Logf("Loaded %d scope priorities", len(priorities))
// Verify a known scope exists (im:message:recall is in the user's data)
if _, ok := priorities["im:message:recall"]; !ok {
t.Error("expected im:message:recall in priorities")
}
}
func TestGetScopeScore(t *testing.T) {
// Known scope should have a real score
score := GetScopeScore("im:message:recall")
if score == DefaultScopeScore {
t.Errorf("expected real score for im:message:recall, got default %d", score)
}
t.Logf("im:message:recall score: %d", score)
// Unknown scope should return default
score = GetScopeScore("unknown:scope:here")
if score != DefaultScopeScore {
t.Errorf("expected %d, got %d", DefaultScopeScore, score)
}
// Override: im:chat:readonly should be overridden to 1
score = GetScopeScore("im:chat:readonly")
if score != 1 {
t.Errorf("expected im:chat:readonly override score 1, got %d", score)
}
}
func TestSelectRecommendedScope_PicksHighestScore(t *testing.T) {
priorities := LoadScopePriorities()
// Find two scopes with known different scores
scopeA := "calendar:calendar:readonly"
scopeB := "calendar:calendar"
scoreA, okA := priorities[scopeA]
scoreB, okB := priorities[scopeB]
if !okA || !okB {
t.Skipf("test scopes not in priorities (A=%v, B=%v)", okA, okB)
}
t.Logf("%s=%d, %s=%d", scopeA, scoreA, scopeB, scoreB)
scopes := []interface{}{scopeB, scopeA}
result := SelectRecommendedScope(scopes, "user")
// Should pick the higher-scored one (higher = more recommended)
if scoreA > scoreB {
if result != scopeA {
t.Errorf("expected %s (score %d), got %s", scopeA, scoreA, result)
}
} else {
if result != scopeB {
t.Errorf("expected %s (score %d), got %s", scopeB, scoreB, result)
}
}
}
func TestSelectRecommendedScope_FallbackToFirst(t *testing.T) {
scopes := []interface{}{
"zzz_unknown:scope:a",
"zzz_unknown:scope:b",
}
result := SelectRecommendedScope(scopes, "user")
// All unknown scopes get DefaultScopeScore; first one with that score wins
if result != "zzz_unknown:scope:a" {
t.Errorf("expected zzz_unknown:scope:a, got %s", result)
}
}
func TestSelectRecommendedScope_Empty(t *testing.T) {
result := SelectRecommendedScope(nil, "user")
if result != "" {
t.Errorf("expected empty string, got %s", result)
}
result = SelectRecommendedScope([]interface{}{}, "user")
if result != "" {
t.Errorf("expected empty string, got %s", result)
}
}
func TestComputeMinimumScopeSet(t *testing.T) {
minSet := ComputeMinimumScopeSet("user")
if len(minSet) == 0 {
if len(ListFromMetaProjects()) == 0 {
t.Skip("no from_meta data available")
}
t.Fatal("expected non-empty minimum scope set")
}
// Verify sorted
if !sort.StringsAreSorted(minSet) {
t.Error("expected sorted result")
}
// Verify no duplicates
seen := make(map[string]bool)
for _, s := range minSet {
if seen[s] {
t.Errorf("duplicate scope: %s", s)
}
seen[s] = true
}
t.Logf("Minimum scope set (%d scopes): %v", len(minSet), minSet)
}
func TestComputeMinimumScopeSet_Tenant(t *testing.T) {
ensureFreshRegistry(t)
minSet := ComputeMinimumScopeSet("tenant")
if len(minSet) == 0 {
if len(ListFromMetaProjects()) == 0 {
t.Skip("no from_meta data available")
}
t.Fatal("expected non-empty minimum scope set for tenant")
}
t.Logf("Tenant minimum scope set (%d scopes): %v", len(minSet), minSet)
}
func TestFilterScopes(t *testing.T) {
scopes := []string{
"calendar:calendar.event:read",
"calendar:calendar:readonly",
"task:task:read",
"drive:drive.metadata:readonly",
}
// Filter by domain
result := FilterScopes(scopes, []string{"calendar"}, nil)
if len(result) != 2 {
t.Errorf("expected 2 calendar scopes, got %d: %v", len(result), result)
}
// Filter by permission
result = FilterScopes(scopes, nil, []string{"read"})
for _, s := range result {
t.Logf("read-filtered: %s", s)
}
}
func TestFilterScopes_WritePermission(t *testing.T) {
scopes := []string{
"calendar:calendar.event:read",
"calendar:calendar:readonly",
"task:task:write",
"drive:drive:writeonly",
"drive:drive:write_only",
}
result := FilterScopes(scopes, nil, []string{"write"})
// "write" matches anything containing "write" (including writeonly, write_only)
if len(result) != 3 {
t.Errorf("expected 3 scopes matching 'write', got %d: %v", len(result), result)
}
result = FilterScopes(scopes, nil, []string{"writeonly"})
if len(result) != 2 {
t.Errorf("expected 2 writeonly scopes, got %d: %v", len(result), result)
}
}
func TestFilterScopes_DomainAndPermission(t *testing.T) {
scopes := []string{
"calendar:calendar.event:read",
"calendar:calendar:readonly",
"task:task:read",
"drive:drive.metadata:readonly",
}
// Filter by domain AND permission
result := FilterScopes(scopes, []string{"calendar"}, []string{"readonly"})
if len(result) != 1 || result[0] != "calendar:calendar:readonly" {
t.Errorf("expected [calendar:calendar:readonly], got %v", result)
}
}
func TestFilterScopes_NilFilters(t *testing.T) {
scopes := []string{"a:b:c", "d:e:f"}
result := FilterScopes(scopes, nil, nil)
if len(result) != 2 {
t.Errorf("expected all scopes returned when no filters, got %d", len(result))
}
}
func TestFilterScopes_Empty(t *testing.T) {
result := FilterScopes(nil, nil, nil)
if result != nil {
t.Errorf("expected nil, got %v", result)
}
}
func TestFilterScopes_TooFewParts(t *testing.T) {
scopes := []string{"onlyonepart", "two:parts"}
// Permission filter requires at least 3 parts
result := FilterScopes(scopes, nil, []string{"read"})
if len(result) != 0 {
t.Errorf("expected 0 results for short scopes, got %v", result)
}
}
// --- Auto-approve functions ---
func TestLoadAutoApproveSet(t *testing.T) {
aaSet := LoadAutoApproveSet()
if len(aaSet) == 0 {
t.Fatal("expected non-empty auto-approve set")
}
// From scope_overrides.json allow list
if !aaSet["calendar:calendar.event:create"] {
t.Error("expected calendar:calendar.event:create in auto-approve set (from allow list)")
}
// Verify allow list entries are present
if !aaSet["sheets:spreadsheet:read"] {
t.Error("expected sheets:spreadsheet:read in auto-approve set (from allow list)")
}
t.Logf("Auto-approve set has %d scopes", len(aaSet))
}
func TestLoadPlatformAutoApproveSet(t *testing.T) {
paaSet := LoadPlatformAutoApproveSet()
// This should only include scopes from scope_priorities.json with AutoApprove rule.
// It does NOT apply deny overrides.
if len(paaSet) == 0 {
t.Fatal("expected non-empty platform auto-approve set")
}
t.Logf("Platform auto-approve set has %d scopes", len(paaSet))
}
func TestLoadOverrideAutoApproveAllow(t *testing.T) {
allowSet := LoadOverrideAutoApproveAllow()
if len(allowSet) == 0 {
t.Fatal("expected non-empty override allow set")
}
// Known entries from scope_overrides.json
if !allowSet["calendar:calendar.event:create"] {
t.Error("expected calendar:calendar.event:create in allow set")
}
if !allowSet["mail:event"] {
t.Error("expected mail:event in allow set")
}
}
func TestLoadOverrideAutoApproveDeny(t *testing.T) {
denySet := LoadOverrideAutoApproveDeny()
// deny list may be empty if all entries are moved to _deny (commented out)
t.Logf("Override deny set has %d scopes", len(denySet))
}
func TestIsAutoApproveScope(t *testing.T) {
// Known auto-approve scope (in allow list)
if !IsAutoApproveScope("calendar:calendar.event:create") {
t.Error("expected calendar:calendar.event:create to be auto-approve")
}
// Completely unknown scope
if IsAutoApproveScope("zzz:unknown:scope") {
t.Error("expected unknown scope to NOT be auto-approve")
}
}
func TestFilterAutoApproveScopes(t *testing.T) {
scopes := []string{
"calendar:calendar.event:create", // auto-approve (in allow list)
"zzz:unknown:scope", // not in auto-approve
"sheets:spreadsheet:read", // auto-approve (in allow list)
}
result := FilterAutoApproveScopes(scopes)
if len(result) < 1 {
t.Fatal("expected at least 1 auto-approve scope in result")
}
// Check that calendar:calendar.event:create is included
found := false
for _, s := range result {
if s == "calendar:calendar.event:create" {
found = true
}
// Ensure unknown scopes are not included
if s == "zzz:unknown:scope" {
t.Error("unknown scope should not be in auto-approve result")
}
}
if !found {
t.Error("expected calendar:calendar.event:create in result")
}
}
func TestFilterAutoApproveScopes_Empty(t *testing.T) {
result := FilterAutoApproveScopes(nil)
if result != nil {
t.Errorf("expected nil, got %v", result)
}
result = FilterAutoApproveScopes([]string{})
if result != nil {
t.Errorf("expected nil for empty input, got %v", result)
}
}
// --- Helper functions ---
func TestGetStrFromMap(t *testing.T) {
m := map[string]interface{}{
"key1": "value1",
"key2": 42,
"key3": nil,
}
if v := GetStrFromMap(m, "key1"); v != "value1" {
t.Errorf("expected value1, got %s", v)
}
if v := GetStrFromMap(m, "key2"); v != "" {
t.Errorf("expected empty for non-string value, got %s", v)
}
if v := GetStrFromMap(m, "missing"); v != "" {
t.Errorf("expected empty for missing key, got %s", v)
}
if v := GetStrFromMap(nil, "key"); v != "" {
t.Errorf("expected empty for nil map, got %s", v)
}
}
func TestGetRegistryDir(t *testing.T) {
dir := GetRegistryDir()
if dir == "" {
t.Error("expected non-empty registry dir")
}
t.Logf("Registry dir: %s", dir)
}
// --- Scope collection functions ---
func TestCollectAllScopesFromMeta(t *testing.T) {
projects := ListFromMetaProjects()
if len(projects) == 0 {
t.Skip("no from_meta data available")
}
allScopes := CollectAllScopesFromMeta("user")
if len(allScopes) == 0 {
t.Fatal("expected non-empty scopes from from_meta")
}
// Should be sorted
if !sort.StringsAreSorted(allScopes) {
t.Error("expected sorted result")
}
// Should include more scopes than the minimum set (since minimum picks best per method)
minSet := ComputeMinimumScopeSet("user")
if len(allScopes) < len(minSet) {
t.Errorf("all scopes (%d) should be >= minimum set (%d)", len(allScopes), len(minSet))
}
t.Logf("All scopes from meta: %d (min set: %d)", len(allScopes), len(minSet))
}
func TestCollectAllScopesFromMeta_Caching(t *testing.T) {
projects := ListFromMetaProjects()
if len(projects) == 0 {
t.Skip("no from_meta data available")
}
result1 := CollectAllScopesFromMeta("user")
result2 := CollectAllScopesFromMeta("user")
if len(result1) != len(result2) {
t.Errorf("cached result length mismatch: %d vs %d", len(result1), len(result2))
}
}
func TestCollectScopesWithSources(t *testing.T) {
projects := ListFromMetaProjects()
if len(projects) == 0 {
t.Skip("no from_meta data available")
}
// Use calendar project which is well-known
scopes, sources := CollectScopesWithSources([]string{"calendar"}, "user")
if len(scopes) == 0 {
t.Fatal("expected non-empty scopes for calendar")
}
// Should be sorted
if !sort.StringsAreSorted(scopes) {
t.Error("expected sorted scopes")
}
// Each scope should have a source
for _, s := range scopes {
src, ok := sources[s]
if !ok {
t.Errorf("scope %s has no source entry", s)
continue
}
if len(src.APIs) == 0 {
t.Errorf("scope %s has no API sources", s)
}
}
t.Logf("Calendar scopes with sources: %d scopes", len(scopes))
}
func TestCollectScopesWithSources_EmptyProject(t *testing.T) {
scopes, sources := CollectScopesWithSources([]string{"nonexistent_project"}, "user")
if len(scopes) != 0 {
t.Errorf("expected empty scopes for nonexistent project, got %d", len(scopes))
}
if len(sources) != 0 {
t.Errorf("expected empty sources for nonexistent project, got %d", len(sources))
}
}
func TestCollectCommandScopes(t *testing.T) {
projects := ListFromMetaProjects()
if len(projects) == 0 {
t.Skip("no from_meta data available")
}
entries := CollectCommandScopes([]string{"calendar"}, "user")
if len(entries) == 0 {
t.Fatal("expected non-empty command entries for calendar")
}
// Verify sorted by Command
for i := 1; i < len(entries); i++ {
if entries[i].Command < entries[i-1].Command {
t.Errorf("entries not sorted: %s < %s", entries[i].Command, entries[i-1].Command)
}
}
// Verify each entry has scopes and type
for _, e := range entries {
if e.Command == "" {
t.Error("entry has empty command")
}
if e.Type != "api" {
t.Errorf("expected type 'api', got %q", e.Type)
}
if len(e.Scopes) == 0 {
t.Errorf("entry %s has no scopes", e.Command)
}
}
t.Logf("Calendar command entries: %d", len(entries))
}
func TestCollectCommandScopes_EmptyProject(t *testing.T) {
entries := CollectCommandScopes([]string{"nonexistent_project"}, "user")
if len(entries) != 0 {
t.Errorf("expected empty entries for nonexistent project, got %d", len(entries))
}
}
func TestGetScopesForDomains(t *testing.T) {
projects := ListFromMetaProjects()
if len(projects) == 0 {
t.Skip("no from_meta data available")
}
// GetScopesForDomains is a wrapper for CollectScopesForProjects
scopes := GetScopesForDomains([]string{"calendar"}, "user")
expected := CollectScopesForProjects([]string{"calendar"}, "user")
if len(scopes) != len(expected) {
t.Errorf("GetScopesForDomains and CollectScopesForProjects differ: %d vs %d", len(scopes), len(expected))
}
}
func TestGetReadOnlyScopes(t *testing.T) {
projects := ListFromMetaProjects()
if len(projects) == 0 {
t.Skip("no from_meta data available")
}
readOnly := GetReadOnlyScopes("user")
// May be empty if no read-only scopes exist, but should not panic
for _, s := range readOnly {
parts := strings.Split(s, ":")
if len(parts) < 3 {
t.Errorf("unexpected scope format (too few parts): %s", s)
continue
}
perm := parts[2]
if !strings.Contains(perm, "read") && perm != "readonly" {
t.Errorf("non-read scope in read-only result: %s", s)
}
}
t.Logf("Read-only scopes: %d", len(readOnly))
}
func TestResolveScopesFromFilters(t *testing.T) {
projects := ListFromMetaProjects()
if len(projects) == 0 {
t.Skip("no from_meta data available")
}
// Should behave like CollectScopesForProjects + FilterScopes
scopes := ResolveScopesFromFilters([]string{"calendar"}, []string{"read", "readonly"}, "user")
for _, s := range scopes {
parts := strings.Split(s, ":")
if len(parts) < 3 {
continue
}
perm := parts[2]
if !strings.Contains(perm, "read") && perm != "readonly" {
t.Errorf("non-read scope in filtered result: %s", s)
}
}
t.Logf("Resolved filtered scopes: %d", len(scopes))
}
func TestCollectScopesForProjects_MultipleProjects(t *testing.T) {
projects := ListFromMetaProjects()
if len(projects) < 2 {
t.Skip("need at least 2 from_meta projects")
}
// Multiple projects should yield more scopes than a single one
single := CollectScopesForProjects(projects[:1], "user")
multi := CollectScopesForProjects(projects[:2], "user")
if len(multi) < len(single) {
t.Errorf("multi-project scopes (%d) should be >= single-project (%d)", len(multi), len(single))
}
}
func TestCollectScopesForProjects_NonexistentProject(t *testing.T) {
scopes := CollectScopesForProjects([]string{"nonexistent_project_xyz"}, "user")
if len(scopes) != 0 {
t.Errorf("expected empty scopes for nonexistent project, got %d", len(scopes))
}
}
// --- auth_domain functions ---
func TestGetAuthDomain_Configured(t *testing.T) {
// whiteboard has auth_domain: "docs" in service_descriptions.json
if got := GetAuthDomain("whiteboard"); got != "docs" {
t.Errorf("GetAuthDomain(whiteboard) = %q, want %q", got, "docs")
}
}
func TestGetAuthDomain_NotConfigured(t *testing.T) {
if got := GetAuthDomain("calendar"); got != "" {
t.Errorf("GetAuthDomain(calendar) = %q, want empty", got)
}
}
func TestGetAuthDomain_Unknown(t *testing.T) {
if got := GetAuthDomain("nonexistent_xyz"); got != "" {
t.Errorf("GetAuthDomain(nonexistent_xyz) = %q, want empty", got)
}
}
func TestHasAuthDomain(t *testing.T) {
if !HasAuthDomain("whiteboard") {
t.Error("HasAuthDomain(whiteboard) = false, want true")
}
if HasAuthDomain("calendar") {
t.Error("HasAuthDomain(calendar) = true, want false")
}
}
func TestGetAuthChildren(t *testing.T) {
children := GetAuthChildren("docs")
found := false
for _, c := range children {
if c == "whiteboard" {
found = true
break
}
}
if !found {
t.Errorf("GetAuthChildren(docs) = %v, want to contain 'whiteboard'", children)
}
}
func TestGetAuthChildren_NoChildren(t *testing.T) {
children := GetAuthChildren("calendar")
if len(children) != 0 {
t.Errorf("GetAuthChildren(calendar) = %v, want empty", children)
}
}