Files
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

183 lines
5.4 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package yaml_test
import (
"reflect"
"testing"
"github.com/larksuite/cli/extension/platform"
pyaml "github.com/larksuite/cli/internal/cmdpolicy/yaml"
)
func TestParse_validRule(t *testing.T) {
data := []byte(`
name: agent-docs-readonly
description: only-read docs
allow:
- docs/**
- contact/**
deny:
- docs/+update
max_risk: read
identities:
- user
`)
rules, err := pyaml.Parse(data)
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
want := &platform.Rule{
Name: "agent-docs-readonly",
Description: "only-read docs",
Allow: []string{"docs/**", "contact/**"},
Deny: []string{"docs/+update"},
MaxRisk: "read",
Identities: []platform.Identity{"user"},
}
// A flat top-level rule yields exactly one element (backward compat).
if !reflect.DeepEqual(rules, []*platform.Rule{want}) {
t.Fatalf("rules = %+v, want single %+v", rules, want)
}
}
// A "rules:" list yields one platform.Rule per entry, in order. This is
// the multi-rule layout: each rule is a scoped grant the engine
// OR-combines.
func TestParse_rulesList(t *testing.T) {
data := []byte(`
rules:
- name: docs-ro
allow: [docs/**]
max_risk: read
- name: im-rw
allow: [im/**]
max_risk: write
identities: [user, bot]
`)
rules, err := pyaml.Parse(data)
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
want := []*platform.Rule{
{Name: "docs-ro", Allow: []string{"docs/**"}, MaxRisk: "read"},
{Name: "im-rw", Allow: []string{"im/**"}, MaxRisk: "write", Identities: []platform.Identity{"user", "bot"}},
}
if !reflect.DeepEqual(rules, want) {
t.Fatalf("rules = %+v, want %+v", rules, want)
}
}
// A "rules:" key that is present but empty is a foot-gun: an empty list
// would otherwise fall through to a single all-zero Rule that allows
// every annotated command ("looks like a policy, enforces almost
// nothing"). Parse must reject it outright instead.
func TestParse_rejectsEmptyRulesList(t *testing.T) {
if _, err := pyaml.Parse([]byte("rules: []\n")); err == nil {
t.Fatalf("Parse should reject a present-but-empty 'rules:' list")
}
}
// Mixing top-level flat rule fields with a rules: list is ambiguous and
// must be rejected rather than silently picking one.
func TestParse_rejectsFlatPlusRulesMix(t *testing.T) {
data := []byte(`
name: top-level
rules:
- name: nested
`)
if _, err := pyaml.Parse(data); err == nil {
t.Fatalf("Parse should reject mixing top-level fields with a rules: list")
}
}
// allow_unannotated is documented in the README / author guide as the
// gradual-adoption opt-in. The yaml schema must carry it through to
// platform.Rule, otherwise a user following the docs would either hit
// "unknown field" (under KnownFields strict mode) or silently lose the
// opt-in and end up with a safer-but-broken policy.
func TestParse_allowUnannotatedPassesThrough(t *testing.T) {
data := []byte(`
name: agent-readonly
max_risk: read
allow_unannotated: true
`)
rules, err := pyaml.Parse(data)
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
if !rules[0].AllowUnannotated {
t.Fatalf("AllowUnannotated = false, want true (yaml field must propagate)")
}
if rules[0].MaxRisk != "read" || rules[0].Name != "agent-readonly" {
t.Errorf("other fields lost: %+v", rules[0])
}
}
// Default is false when the key is absent: pin the fail-closed default so
// future schema edits cannot accidentally flip it.
func TestParse_allowUnannotatedDefaultsFalse(t *testing.T) {
data := []byte(`
name: x
max_risk: read
`)
rules, err := pyaml.Parse(data)
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
if rules[0].AllowUnannotated {
t.Fatalf("AllowUnannotated must default to false when key is absent")
}
}
// Unknown fields must be rejected so the old binary cannot silently ignore
// new schema additions (forward-compat safeguard).
func TestParse_rejectsUnknownFields(t *testing.T) {
data := []byte(`
name: x
mystery_field: oh no
`)
if _, err := pyaml.Parse(data); err == nil {
t.Fatalf("Parse should reject unknown yaml field 'mystery_field'")
}
}
// Semantic validation lives in cmdpolicy.ValidateRule. Parse only checks
// structural yaml; an invalid max_risk passes through (validation happens
// downstream).
func TestParse_doesNotValidateSemantics(t *testing.T) {
rules, err := pyaml.Parse([]byte("max_risk: nuclear\n"))
if err != nil {
t.Fatalf("structural parse should succeed, got %v", err)
}
if rules[0].MaxRisk != "nuclear" {
t.Fatalf("MaxRisk = %q, want passed through as-is", rules[0].MaxRisk)
}
}
// An entirely empty file is rejected: the resolver should fall back to
// "no rule" by skipping the file in the first place, not by feeding empty
// bytes through Parse.
func TestParse_emptyIsError(t *testing.T) {
if _, err := pyaml.Parse([]byte{}); err == nil {
t.Fatalf("Parse should reject empty input; the resolver handles 'no file' separately")
}
}
// A stray "---" separator followed by another document would silently
// drop the trailing rule if yaml.v3 stopped after the first Decode.
// Parse must reject multi-document input so the operator can't typo a
// separator and end up with an unintentionally empty policy.
func TestParse_rejectsMultipleDocuments(t *testing.T) {
data := []byte(`name: first
max_risk: read
---
name: second
max_risk: write
`)
if _, err := pyaml.Parse(data); err == nil {
t.Fatalf("Parse should reject multi-document YAML input")
}
}