mirror of
https://github.com/certimate-go/certimate.git
synced 2026-09-06 16:05:07 +08:00
259 lines
7.0 KiB
Go
259 lines
7.0 KiB
Go
package plugin
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"log/slog"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
)
|
|
|
|
var (
|
|
fakeBinaryOnce sync.Once
|
|
fakeBinaryPath string
|
|
fakeBinaryErr error
|
|
)
|
|
|
|
func fakeBinary(t *testing.T) string {
|
|
t.Helper()
|
|
fakeBinaryOnce.Do(func() {
|
|
out := filepath.Join(os.TempDir(), "certimate-fakeplugin-test")
|
|
if runtime.GOOS == "windows" {
|
|
out += ".exe"
|
|
}
|
|
cmd := exec.Command("go", "build", "-tags=fakeplugin", "-o", out, "./testplugin")
|
|
cmd.Dir = "."
|
|
if buildOut, err := cmd.CombinedOutput(); err != nil {
|
|
fakeBinaryErr = errors.New(strings.TrimSpace(string(buildOut)) + " | " + err.Error())
|
|
return
|
|
}
|
|
fakeBinaryPath = out
|
|
})
|
|
if fakeBinaryErr != nil {
|
|
t.Skipf("fakeplugin build failed: %v", fakeBinaryErr)
|
|
}
|
|
return fakeBinaryPath
|
|
}
|
|
|
|
func discoveredFake(t *testing.T, providerType string) *DiscoveredPlugin {
|
|
t.Helper()
|
|
bin := fakeBinary(t)
|
|
dir := t.TempDir()
|
|
manifest := manifestJSON(func(m map[string]any) {
|
|
m["provider_type"] = providerType
|
|
m["access_provider_type"] = providerType
|
|
m["binary"] = "fakeplugin"
|
|
})
|
|
if err := os.WriteFile(filepath.Join(dir, "manifest.json"), []byte(manifest), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
link := filepath.Join(dir, "fakeplugin")
|
|
if err := os.Symlink(bin, link); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
dp, ferr := discoverOne(dir, "0.1.0")
|
|
if ferr != nil {
|
|
t.Fatalf("discoverOne: %v", ferr.Err)
|
|
}
|
|
return dp
|
|
}
|
|
|
|
func withFakeEnv(providerType, behavior string) func() {
|
|
os.Setenv("FAKEPLUGIN_PROVIDER_TYPE", providerType)
|
|
os.Setenv("FAKEPLUGIN_BEHAVIOR", behavior)
|
|
return func() {
|
|
os.Unsetenv("FAKEPLUGIN_PROVIDER_TYPE")
|
|
os.Unsetenv("FAKEPLUGIN_BEHAVIOR")
|
|
}
|
|
}
|
|
|
|
func TestManager_Bootstrap_FetchesMetadataAndSchema(t *testing.T) {
|
|
defer withFakeEnv("bootstrap-demo", "ok")()
|
|
dp := discoveredFake(t, "bootstrap-demo")
|
|
|
|
mgr := NewManager(PluginConfig{}, nil)
|
|
meta, schema, err := mgr.Bootstrap(context.Background(), dp)
|
|
if err != nil {
|
|
t.Fatalf("bootstrap: %v", err)
|
|
}
|
|
if meta.ProviderType != "bootstrap-demo" || meta.ProtocolVersion != ProtocolVersion {
|
|
t.Fatalf("metadata mismatch: %+v", meta)
|
|
}
|
|
if schema == nil || len(schema.DeploySchemaJSON) == 0 {
|
|
t.Fatalf("schema not fetched: %+v", schema)
|
|
}
|
|
if schema.I18n["en"]["plugin.fake.name"] == "" {
|
|
t.Fatalf("i18n not fetched: %+v", schema.I18n)
|
|
}
|
|
}
|
|
|
|
func TestManager_Deploy_OnDemandLifecycle(t *testing.T) {
|
|
defer withFakeEnv("deploy-demo", "ok")()
|
|
dp := discoveredFake(t, "deploy-demo")
|
|
|
|
mgr := NewManager(PluginConfig{}, nil)
|
|
res1, err := mgr.Deploy(context.Background(), dp, sampleDeployReq(), nil)
|
|
if err != nil {
|
|
t.Fatalf("first deploy: %v", err)
|
|
}
|
|
if res1.ExtendedDataJSON == "" {
|
|
t.Fatal("empty deploy result")
|
|
}
|
|
res2, err := mgr.Deploy(context.Background(), dp, sampleDeployReq(), nil)
|
|
if err != nil {
|
|
t.Fatalf("second deploy (fresh client): %v", err)
|
|
}
|
|
if res2 == nil {
|
|
t.Fatal("nil second result")
|
|
}
|
|
}
|
|
|
|
func TestManager_Deploy_PluginConfigError_Mapped(t *testing.T) {
|
|
defer withFakeEnv("cfgerr-demo", "configerror")()
|
|
dp := discoveredFake(t, "cfgerr-demo")
|
|
|
|
mgr := NewManager(PluginConfig{}, nil)
|
|
_, err := mgr.Deploy(context.Background(), dp, sampleDeployReq(), nil)
|
|
if err == nil {
|
|
t.Fatal("expected config error")
|
|
}
|
|
if !strings.Contains(err.Error(), "bad config") {
|
|
t.Fatalf("expected config error message preserved, got %q", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestSyncBuffer_ConcurrentWriteAndTail(t *testing.T) {
|
|
var b syncBuffer
|
|
|
|
const line = "stderr line\n"
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 4; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for j := 0; j < 100; j++ {
|
|
b.Write([]byte(line))
|
|
}
|
|
}()
|
|
}
|
|
for i := 0; i < 100; i++ {
|
|
b.tail(stderrTailLimit)
|
|
}
|
|
wg.Wait()
|
|
|
|
if got, want := len(b.tail(stderrTailLimit)), 4*100*len(line); got != want {
|
|
t.Fatalf("tail length: got %d, want %d", got, want)
|
|
}
|
|
|
|
for i := 0; i < 3*stderrTailLimit/len(line); i++ {
|
|
b.Write([]byte(line))
|
|
}
|
|
if b.buf.Len() > 2*stderrTailLimit {
|
|
t.Fatalf("compacted buffer length: got %d, want <= %d", b.buf.Len(), 2*stderrTailLimit)
|
|
}
|
|
tail := b.tail(stderrTailLimit)
|
|
if len(tail) != stderrTailLimit {
|
|
t.Fatalf("capped tail length: got %d, want %d", len(tail), stderrTailLimit)
|
|
}
|
|
if !bytes.HasSuffix(tail, []byte(line)) {
|
|
t.Fatal("capped tail should end with a complete line")
|
|
}
|
|
}
|
|
|
|
func TestManager_Deploy_CrashIsolated_ReturnsErrPluginCrashed(t *testing.T) {
|
|
defer withFakeEnv("crash-demo", "crash")()
|
|
dp := discoveredFake(t, "crash-demo")
|
|
|
|
mgr := NewManager(PluginConfig{}, nil)
|
|
_, err := mgr.Deploy(context.Background(), dp, sampleDeployReq(), nil)
|
|
if err == nil {
|
|
t.Fatal("expected crash error")
|
|
}
|
|
var crashed *ErrPluginCrashed
|
|
if !errors.As(err, &crashed) {
|
|
t.Fatalf("expected ErrPluginCrashed, got %T: %v", err, err)
|
|
}
|
|
if crashed.ProviderType != "crash-demo" {
|
|
t.Fatalf("crash error wrong provider: %q", crashed.ProviderType)
|
|
}
|
|
if crashed.StderrTail == "" {
|
|
t.Fatal("expected non-empty stderr tail")
|
|
}
|
|
}
|
|
|
|
func TestManager_Deploy_CrashRedactsCredentials(t *testing.T) {
|
|
defer withFakeEnv("crash-redact", "crash")()
|
|
dp := discoveredFake(t, "crash-redact")
|
|
|
|
const secret = "supersecrettoken1234"
|
|
req := &DeployRequest{
|
|
LogLevel: "INFO",
|
|
AccessConfigJSON: `{"url":"https://example.com","secret":"` + secret + `"}`,
|
|
ExtendedConfigJSON: `{"path":"/x"}`,
|
|
CertificatePEM: "CERT",
|
|
PrivateKeyPEM: "KEY",
|
|
}
|
|
|
|
mgr := NewManager(PluginConfig{}, nil)
|
|
_, err := mgr.Deploy(context.Background(), dp, req, nil)
|
|
var crashed *ErrPluginCrashed
|
|
if !errors.As(err, &crashed) {
|
|
t.Fatalf("expected ErrPluginCrashed, got %T: %v", err, err)
|
|
}
|
|
if strings.Contains(crashed.StderrTail, secret) {
|
|
t.Fatalf("secret leaked into crash stderr tail:\n%s", crashed.StderrTail)
|
|
}
|
|
}
|
|
|
|
func TestRedactorFor_RedactsPEM(t *testing.T) {
|
|
body := strings.Repeat("A", 64)
|
|
key := "-----BEGIN PRIVATE KEY-----\n" + body + "\n-----END PRIVATE KEY-----"
|
|
redact := redactorFor(&DeployRequest{PrivateKeyPEM: key})
|
|
|
|
fullOut := redact("leaked: " + key)
|
|
if strings.Contains(fullOut, body) {
|
|
t.Fatalf("private key body not redacted when full PEM logged: %q", fullOut)
|
|
}
|
|
bodyOut := redact("leaked body: " + body)
|
|
if strings.Contains(bodyOut, body) {
|
|
t.Fatalf("private key body line not redacted: %q", bodyOut)
|
|
}
|
|
}
|
|
|
|
func TestManager_Deploy_ForwardsPluginLogs(t *testing.T) {
|
|
defer withFakeEnv("forward-demo", "ok")()
|
|
dp := discoveredFake(t, "forward-demo")
|
|
|
|
mgr := NewManager(PluginConfig{}, nil)
|
|
cap := &captureHandler{}
|
|
_, err := mgr.Deploy(context.Background(), dp, sampleDeployReq(), slog.New(cap))
|
|
if err != nil {
|
|
t.Fatalf("deploy: %v", err)
|
|
}
|
|
found := false
|
|
for _, r := range cap.records {
|
|
if r.Message == "fakeplugin deploy starting" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("plugin log not forwarded to sink: %+v", cap.records)
|
|
}
|
|
}
|
|
|
|
func sampleDeployReq() *DeployRequest {
|
|
return &DeployRequest{
|
|
LogLevel: "INFO",
|
|
AccessConfigJSON: `{"url":"https://example.com","secret":"tok"}`,
|
|
ExtendedConfigJSON: `{"method":"POST"}`,
|
|
CertificatePEM: "CERT",
|
|
PrivateKeyPEM: "KEY",
|
|
}
|
|
}
|