maidn-cli/internal/openbao/bootstrap_test.go

126 lines
5.3 KiB
Go

package openbao
import (
"encoding/base64"
"errors"
"os/exec"
"path/filepath"
"strings"
"testing"
)
func TestEnsureRecoveryIdentity(t *testing.T) {
if _, err := exec.LookPath("age-keygen"); err != nil {
t.Skip("age-keygen is required for OpenBao recovery setup")
}
recipient, err := EnsureRecoveryIdentity(filepath.Join(t.TempDir(), "recovery-key.txt"))
if err != nil {
t.Fatal(err)
}
if !strings.HasPrefix(recipient, "age1") {
t.Fatalf("invalid recovery recipient")
}
}
func TestUnsealUsesPromptedStdinOnly(t *testing.T) {
originalExec, originalStatus := execInPod, openBaoStatus
t.Cleanup(func() { execInPod, openBaoStatus = originalExec, originalStatus })
calls := 0
execInPod = func(_ string, input []byte, args ...string) ([]byte, error) {
if len(input) == 0 || len(args) != 3 || args[0] != "sh" || args[1] != "-ec" || args[2] != "read -r key; bao operator unseal \"$key\" >/dev/null" {
t.Fatal("unseal share was not submitted through prompted stdin")
}
calls++
return nil, nil
}
openBaoStatus = func(string) (status, error) { return status{Initialized: true}, nil }
if err := unseal("kubeconfig", RecoveryMaterial{UnsealKeysB64: []string{"share-1", "share-2", "share-3"}, UnsealThreshold: 2}); err != nil || calls != 3 {
t.Fatalf("unseal calls:%d err:%v", calls, err)
}
}
func TestUnsealFallsBackToControllerSecret(t *testing.T) {
originalExec, originalStatus, originalController := execInPod, openBaoStatus, execInUnsealController
t.Cleanup(func() {
execInPod, openBaoStatus, execInUnsealController = originalExec, originalStatus, originalController
})
shares := 0
execInPod = func(_ string, _ []byte, _ ...string) ([]byte, error) { shares++; return nil, nil }
statusChecks := 0
openBaoStatus = func(string) (status, error) {
statusChecks++
return status{Initialized: true, Sealed: statusChecks == 1}, nil
}
controllerCalled := false
execInUnsealController = func(_ string, script string) ([]byte, error) {
controllerCalled = strings.Contains(script, "/unseal/unseal-*")
return nil, nil
}
if err := unseal("kubeconfig", RecoveryMaterial{UnsealKeysB64: []string{"share-1", "share-2", "share-3"}, UnsealThreshold: 2}); err != nil || shares != 3 || !controllerCalled {
t.Fatalf("unseal fallback = shares:%d controller:%t err:%v", shares, controllerCalled, err)
}
}
func TestWriteSecretFramesMultilineValues(t *testing.T) {
original := execInPod
t.Cleanup(func() { execInPod = original })
var input, script string
execInPod = func(_ string, contents []byte, args ...string) ([]byte, error) {
input, script = string(contents), args[len(args)-1]
return nil, nil
}
value := "tunnel: tunnel\ningress:\n - service: http_status:404\n"
if err := writeSecret("kubeconfig", "root", "platform/cloudflare-tunnel", map[string]string{"config": value}); err != nil || input != "root\n"+base64.StdEncoding.EncodeToString([]byte(value))+"\n" || !strings.Contains(script, "base64 -d") {
t.Fatalf("multiline secret boundary was not framed safely: %v", err)
}
}
func TestRefreshExternalSecretsIsReadyGatedAndScoped(t *testing.T) {
original := kubectlOutput
t.Cleanup(func() { kubectlOutput = original })
var calls []string
kubectlOutput = func(_ string, args ...string) ([]byte, error) {
calls = append(calls, strings.Join(args, " "))
if len(calls) == 1 {
return []byte("True"), nil
}
return nil, nil
}
if err := refreshExternalSecrets("kubeconfig"); err != nil || len(calls) != 3 || !strings.Contains(calls[0], "get deployment/external-secrets") || !strings.Contains(calls[1], "annotate clustersecretstore openbao") || !strings.Contains(calls[2], "annotate externalsecret forgejo-webhook") || strings.Contains(calls[1], "--all") || strings.Contains(calls[2], "--all") {
t.Fatalf("ExternalSecret refresh was not readiness-gated and scoped: %q, %v", calls, err)
}
}
func TestReadRecoveryMaterialDecryptsAndValidatesBundle(t *testing.T) {
original := decryptRecovery
t.Cleanup(func() { decryptRecovery = original })
called := false
decryptRecovery = func(identityPath, bundlePath string) ([]byte, error) {
called = identityPath == "recovery-identity" && bundlePath == "recovery-bundle"
return []byte(`{"unseal_keys_b64":["test-share-1","test-share-2"],"unseal_threshold":2,"root_token":"test-root"}`), nil
}
material, err := ReadRecoveryMaterial("recovery-identity", "recovery-bundle")
if err != nil || !called || material.UnsealThreshold != 2 || len(material.UnsealKeysB64) != 2 || material.RootToken == "" {
t.Fatal("valid OpenBao recovery material was not read")
}
}
func TestReadRecoveryMaterialRejectsMalformedOrInsufficientBundle(t *testing.T) {
original := decryptRecovery
t.Cleanup(func() { decryptRecovery = original })
for _, plaintext := range [][]byte{
[]byte(`{"unseal_keys_b64":`),
[]byte(`{"unseal_keys_b64":["test-share"],"unseal_threshold":2,"root_token":"test-root"}`),
} {
decryptRecovery = func(string, string) ([]byte, error) { return plaintext, nil }
if _, err := ReadRecoveryMaterial("recovery-identity", "recovery-bundle"); err == nil {
t.Fatal("invalid OpenBao recovery material was accepted")
}
}
decryptRecovery = func(string, string) ([]byte, error) { return nil, errors.New("unavailable") }
if _, err := ReadRecoveryMaterial("recovery-identity", "recovery-bundle"); err == nil {
t.Fatal("recovery decryption failure was accepted")
}
}