mirror of
https://github.com/larksuite/cli.git
synced 2026-08-03 08:32:46 +08:00
340 lines
13 KiB
Go
340 lines
13 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package plugin_e2e
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/tidwall/gjson"
|
|
)
|
|
|
|
// multipleRestrictPlugin registers TWO distinct plugins that each call
|
|
// Restrict() with an independently valid Rule. cmdpolicy.Resolve rejects
|
|
// more than one distinct Restrict-owner regardless of each rule's own
|
|
// validity (internal/cmdpolicy/resolver.go's distinctOwners check runs
|
|
// before ValidateRule).
|
|
const multipleRestrictPlugin = `// Code generated by plugin_e2e; DO NOT EDIT.
|
|
package plugin
|
|
|
|
import "github.com/larksuite/cli/extension/platform"
|
|
|
|
func init() {
|
|
platform.Register(
|
|
platform.NewPlugin("restrict-a", "0.1.0").
|
|
Restrict(&platform.Rule{
|
|
Name: "a-rule",
|
|
Allow: []string{"docs/**"},
|
|
MaxRisk: platform.RiskRead,
|
|
}).
|
|
MustBuild())
|
|
platform.Register(
|
|
platform.NewPlugin("restrict-b", "0.1.0").
|
|
Restrict(&platform.Rule{
|
|
Name: "b-rule",
|
|
Allow: []string{"im/**"},
|
|
MaxRisk: platform.RiskRead,
|
|
}).
|
|
MustBuild())
|
|
}
|
|
`
|
|
|
|
// TestInstallMultipleRestrictPluginsPin pins reason_code=multiple_restrict_plugins.
|
|
// Observed real output (any command, e.g. "schema" -- the fatal guard walks
|
|
// every RunE in the tree so it fires regardless of which command runs):
|
|
//
|
|
// exit=2
|
|
// stderr={"ok":false,"error":{"type":"validation","subtype":"failed_precondition",
|
|
// "message":"multiple plugins called Restrict; only one plugin may own the
|
|
// policy: [restrict-a restrict-b]",
|
|
// "hint":"plugin policy configuration is broken (reason_code
|
|
// multiple_restrict_plugins); fix the plugin's Restrict rule or remove the
|
|
// conflicting plugin"}}
|
|
func TestInstallMultipleRestrictPluginsPin(t *testing.T) {
|
|
bin := buildFork(t, "multiple-restrict", multipleRestrictPlugin)
|
|
res := run(t, bin, "schema")
|
|
t.Logf("exit=%d stdout=%s stderr=%s", res.exit, res.stdout, res.stderr)
|
|
assertReasonCodeEnvelope(t, res, "multiple_restrict_plugins")
|
|
}
|
|
|
|
// invalidRulePlugin registers a single plugin whose Restrict Rule carries a
|
|
// syntactically-invalid MaxRisk value. Neither the Builder nor the staging
|
|
// Registrar validate Rule *contents* (only nilness) -- semantic validation
|
|
// happens later, in cmdpolicy.ValidateRule, called from
|
|
// cmd/platform_bootstrap.go's applyUserPolicyPruning -> cmdpolicy.Resolve.
|
|
const invalidRulePlugin = `// Code generated by plugin_e2e; DO NOT EDIT.
|
|
package plugin
|
|
|
|
import "github.com/larksuite/cli/extension/platform"
|
|
|
|
func init() {
|
|
platform.Register(
|
|
platform.NewPlugin("invalid-rule", "0.1.0").
|
|
Restrict(&platform.Rule{
|
|
Name: "bad-risk",
|
|
Allow: []string{"docs/**"},
|
|
MaxRisk: platform.Risk("bogus"),
|
|
}).
|
|
MustBuild())
|
|
}
|
|
`
|
|
|
|
// TestInstallInvalidRulePin pins reason_code=invalid_rule. Observed real output
|
|
// (schema):
|
|
//
|
|
// exit=2
|
|
// stderr={"ok":false,"error":{"type":"validation","subtype":"failed_precondition",
|
|
// "message":"plugin \"invalid-rule\" rule invalid: invalid max_risk \"bogus\":
|
|
// must be one of read|write|high-risk-write",
|
|
// "hint":"plugin policy configuration is broken (reason_code invalid_rule);
|
|
// fix the plugin's Restrict rule or remove the conflicting plugin"}}
|
|
func TestInstallInvalidRulePin(t *testing.T) {
|
|
bin := buildFork(t, "invalid-rule", invalidRulePlugin)
|
|
res := run(t, bin, "schema")
|
|
t.Logf("exit=%d stdout=%s stderr=%s", res.exit, res.stdout, res.stderr)
|
|
assertReasonCodeEnvelope(t, res, "invalid_rule")
|
|
}
|
|
|
|
// installFailedPlugin is a hand-written bare platform.Plugin (not
|
|
// Builder-based -- Install returning a plain error is not expressible
|
|
// through the Builder's fluent API) whose Install always returns an error.
|
|
// FailurePolicy=FailClosed makes the host abort rather than warn+skip.
|
|
const installFailedPlugin = `// Code generated by plugin_e2e; DO NOT EDIT.
|
|
package plugin
|
|
|
|
import (
|
|
"errors"
|
|
|
|
"github.com/larksuite/cli/extension/platform"
|
|
)
|
|
|
|
type installFailed struct{}
|
|
|
|
func (installFailed) Name() string { return "install-failed" }
|
|
func (installFailed) Version() string { return "0.1.0" }
|
|
func (installFailed) Capabilities() platform.Capabilities {
|
|
return platform.Capabilities{FailurePolicy: platform.FailClosed}
|
|
}
|
|
func (installFailed) Install(r platform.Registrar) error {
|
|
return errors.New("deliberate install failure")
|
|
}
|
|
|
|
func init() { platform.Register(installFailed{}) }
|
|
`
|
|
|
|
// TestInstallFailedPin pins reason_code=install_failed. Observed real output
|
|
// (schema):
|
|
//
|
|
// exit=2
|
|
// stderr={"ok":false,"error":{"type":"validation","subtype":"failed_precondition",
|
|
// "message":"plugin \"install-failed\" (install_failed): Install returned
|
|
// error: deliberate install failure",
|
|
// "hint":"plugin \"install-failed\" failed to install (reason_code
|
|
// install_failed); fix or remove the plugin before running commands"}}
|
|
func TestInstallFailedPin(t *testing.T) {
|
|
bin := buildFork(t, "install-failed", installFailedPlugin)
|
|
res := run(t, bin, "schema")
|
|
t.Logf("exit=%d stdout=%s stderr=%s", res.exit, res.stdout, res.stderr)
|
|
assertReasonCodeEnvelope(t, res, "install_failed")
|
|
}
|
|
|
|
// installPanicPlugin is a hand-written bare Plugin whose Install panics.
|
|
// safeCallInstall (internal/platform/host.go) recovers and converts the
|
|
// panic into a typed install_panic error rather than crashing the binary.
|
|
const installPanicPlugin = `// Code generated by plugin_e2e; DO NOT EDIT.
|
|
package plugin
|
|
|
|
import "github.com/larksuite/cli/extension/platform"
|
|
|
|
type installPanic struct{}
|
|
|
|
func (installPanic) Name() string { return "install-panic" }
|
|
func (installPanic) Version() string { return "0.1.0" }
|
|
func (installPanic) Capabilities() platform.Capabilities {
|
|
return platform.Capabilities{FailurePolicy: platform.FailClosed}
|
|
}
|
|
func (installPanic) Install(r platform.Registrar) error {
|
|
panic("deliberate install panic")
|
|
}
|
|
|
|
func init() { platform.Register(installPanic{}) }
|
|
`
|
|
|
|
// TestInstallPanicPin pins reason_code=install_panic. Observed real output
|
|
// (schema):
|
|
//
|
|
// exit=2
|
|
// stderr={"ok":false,"error":{"type":"validation","subtype":"failed_precondition",
|
|
// "message":"plugin \"install-panic\" (install_panic): Install panicked:
|
|
// deliberate install panic",
|
|
// "hint":"plugin \"install-panic\" failed to install (reason_code
|
|
// install_panic); fix or remove the plugin before running commands"}}
|
|
func TestInstallPanicPin(t *testing.T) {
|
|
bin := buildFork(t, "install-panic", installPanicPlugin)
|
|
res := run(t, bin, "schema")
|
|
t.Logf("exit=%d stdout=%s stderr=%s", res.exit, res.stdout, res.stderr)
|
|
assertReasonCodeEnvelope(t, res, "install_panic")
|
|
}
|
|
|
|
// pluginNamePanicPlugin is a hand-written bare Plugin whose Name() panics.
|
|
// InstallAll's outer loop calls safeCallName BEFORE it ever reads
|
|
// Capabilities(), so this aborts unconditionally regardless of what
|
|
// Capabilities() would have declared (host.go's isUntrustedConfigError
|
|
// list) -- Capabilities() here is a throwaway zero value, never invoked.
|
|
const pluginNamePanicPlugin = `// Code generated by plugin_e2e; DO NOT EDIT.
|
|
package plugin
|
|
|
|
import "github.com/larksuite/cli/extension/platform"
|
|
|
|
type pluginNamePanic struct{}
|
|
|
|
func (pluginNamePanic) Name() string { panic("deliberate name panic") }
|
|
func (pluginNamePanic) Version() string { return "0.1.0" }
|
|
func (pluginNamePanic) Capabilities() platform.Capabilities {
|
|
return platform.Capabilities{}
|
|
}
|
|
func (pluginNamePanic) Install(r platform.Registrar) error { return nil }
|
|
|
|
func init() { platform.Register(pluginNamePanic{}) }
|
|
`
|
|
|
|
// TestInstallPluginNamePanicPin pins reason_code=plugin_name_panic. Observed real
|
|
// output (schema):
|
|
//
|
|
// exit=2
|
|
// stderr={"ok":false,"error":{"type":"validation","subtype":"failed_precondition",
|
|
// "message":"plugin \"<unknown>\" (plugin_name_panic): Plugin.Name()
|
|
// panicked: deliberate name panic",
|
|
// "hint":"plugin \"<unknown>\" failed to install (reason_code
|
|
// plugin_name_panic); fix or remove the plugin before running commands"}}
|
|
func TestInstallPluginNamePanicPin(t *testing.T) {
|
|
bin := buildFork(t, "plugin-name-panic", pluginNamePanicPlugin)
|
|
res := run(t, bin, "schema")
|
|
t.Logf("exit=%d stdout=%s stderr=%s", res.exit, res.stdout, res.stderr)
|
|
assertReasonCodeEnvelope(t, res, "plugin_name_panic")
|
|
}
|
|
|
|
// capabilitiesPanicPlugin is a hand-written bare Plugin whose Capabilities()
|
|
// panics. readFailurePolicy (internal/platform/host.go) re-invokes
|
|
// Capabilities() to decide FailOpen vs FailClosed, panics again, and its
|
|
// recover leaves the pre-set FailClosed default in place -- so this aborts
|
|
// unconditionally too, without the plugin ever declaring a real policy.
|
|
const capabilitiesPanicPlugin = `// Code generated by plugin_e2e; DO NOT EDIT.
|
|
package plugin
|
|
|
|
import "github.com/larksuite/cli/extension/platform"
|
|
|
|
type capabilitiesPanic struct{}
|
|
|
|
func (capabilitiesPanic) Name() string { return "capabilities-panic" }
|
|
func (capabilitiesPanic) Version() string { return "0.1.0" }
|
|
func (capabilitiesPanic) Capabilities() platform.Capabilities {
|
|
panic("deliberate capabilities panic")
|
|
}
|
|
func (capabilitiesPanic) Install(r platform.Registrar) error { return nil }
|
|
|
|
func init() { platform.Register(capabilitiesPanic{}) }
|
|
`
|
|
|
|
// TestInstallCapabilitiesPanicPin pins reason_code=capabilities_panic. Observed
|
|
// real output (schema):
|
|
//
|
|
// exit=2
|
|
// stderr={"ok":false,"error":{"type":"validation","subtype":"failed_precondition",
|
|
// "message":"plugin \"capabilities-panic\" (capabilities_panic):
|
|
// Plugin.Capabilities() panicked: deliberate capabilities panic",
|
|
// "hint":"plugin \"capabilities-panic\" failed to install (reason_code
|
|
// capabilities_panic); fix or remove the plugin before running commands"}}
|
|
func TestInstallCapabilitiesPanicPin(t *testing.T) {
|
|
bin := buildFork(t, "capabilities-panic", capabilitiesPanicPlugin)
|
|
res := run(t, bin, "schema")
|
|
t.Logf("exit=%d stdout=%s stderr=%s", res.exit, res.stdout, res.stderr)
|
|
assertReasonCodeEnvelope(t, res, "capabilities_panic")
|
|
}
|
|
|
|
// restrictsMismatchPlugin is a hand-written bare Plugin that declares
|
|
// Capabilities.Restricts=true (paired with the required FailClosed) but
|
|
// whose Install never calls r.Restrict. stagingRegistrar.validateSelf
|
|
// (internal/platform/staging.go) checks this exact declared-vs-actual
|
|
// consistency after Install returns.
|
|
const restrictsMismatchPlugin = `// Code generated by plugin_e2e; DO NOT EDIT.
|
|
package plugin
|
|
|
|
import "github.com/larksuite/cli/extension/platform"
|
|
|
|
type restrictsMismatch struct{}
|
|
|
|
func (restrictsMismatch) Name() string { return "restricts-mismatch" }
|
|
func (restrictsMismatch) Version() string { return "0.1.0" }
|
|
func (restrictsMismatch) Capabilities() platform.Capabilities {
|
|
return platform.Capabilities{Restricts: true, FailurePolicy: platform.FailClosed}
|
|
}
|
|
func (restrictsMismatch) Install(r platform.Registrar) error { return nil }
|
|
|
|
func init() { platform.Register(restrictsMismatch{}) }
|
|
`
|
|
|
|
// TestInstallRestrictsMismatchPin pins reason_code=restricts_mismatch.
|
|
// Observed real output (schema):
|
|
//
|
|
// exit=2
|
|
// stderr={"ok":false,"error":{"type":"validation","subtype":"failed_precondition",
|
|
// "message":"plugin \"restricts-mismatch\" (restricts_mismatch):
|
|
// Capabilities.Restricts=true but Install did not call r.Restrict",
|
|
// "hint":"plugin \"restricts-mismatch\" failed to install (reason_code
|
|
// restricts_mismatch); fix or remove the plugin before running commands"}}
|
|
func TestInstallRestrictsMismatchPin(t *testing.T) {
|
|
bin := buildFork(t, "restricts-mismatch", restrictsMismatchPlugin)
|
|
res := run(t, bin, "schema")
|
|
t.Logf("exit=%d stdout=%s stderr=%s", res.exit, res.stdout, res.stderr)
|
|
assertReasonCodeEnvelope(t, res, "restricts_mismatch")
|
|
}
|
|
|
|
// mustBuildPanicPlugin calls MustBuild() on a Builder with an invalid plugin
|
|
// name ("BadName!!" fails ^[a-z0-9][a-z0-9-]*$). This panics from
|
|
// plugin.init(), which runs from the blank-import BEFORE main() has a
|
|
// chance to install any recover-and-envelope guard -- so, unlike every
|
|
// other case here, this crashes the process outright: no JSON envelope,
|
|
// non-zero exit, a raw Go panic trace on stderr.
|
|
const mustBuildPanicPlugin = `// Code generated by plugin_e2e; DO NOT EDIT.
|
|
package plugin
|
|
|
|
import "github.com/larksuite/cli/extension/platform"
|
|
|
|
func init() {
|
|
platform.Register(platform.NewPlugin("BadName!!", "0.1.0").MustBuild())
|
|
}
|
|
`
|
|
|
|
// TestInstallMustBuildInitPanicCrashesBinary pins the MustBuild init-panic crash
|
|
// shape. This is NOT the plugin_install envelope -- it is a bare Go panic
|
|
// crash, because it happens in init(), before main()'s recover guard
|
|
// exists. Observed real output (schema):
|
|
//
|
|
// exit=2
|
|
// stderr=panic: plugin "BadName!!": invalid plugin name "BadName!!": must
|
|
// match ^[a-z0-9][a-z0-9-]*$
|
|
//
|
|
// goroutine 1 [running]:
|
|
// larkcustomer/plugin.init.0(...)
|
|
// .../plugin/plugin.go:7
|
|
// ...
|
|
func TestInstallMustBuildInitPanicCrashesBinary(t *testing.T) {
|
|
bin := buildFork(t, "mustbuild-panic", mustBuildPanicPlugin)
|
|
res := run(t, bin, "schema")
|
|
t.Logf("exit=%d stdout=%s stderr=%s", res.exit, res.stdout, res.stderr)
|
|
if res.exit == 0 {
|
|
t.Fatalf("expected non-zero exit on init panic; exit=%d stdout=%s stderr=%s", res.exit, res.stdout, res.stderr)
|
|
}
|
|
if gjson.Valid(res.stderr) {
|
|
t.Fatalf("expected a raw panic trace, not a JSON envelope; stderr=%s", res.stderr)
|
|
}
|
|
if !strings.Contains(res.stderr, "panic:") {
|
|
t.Fatalf("stderr missing Go panic trace; stderr=%s", res.stderr)
|
|
}
|
|
if !strings.Contains(res.stderr, `invalid plugin name "BadName!!"`) {
|
|
t.Errorf("stderr missing the Builder's invalid-name message; stderr=%s", res.stderr)
|
|
}
|
|
}
|