248 lines
9.2 KiB
Go
248 lines
9.2 KiB
Go
package app
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
|
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
|
)
|
|
|
|
func TestPipelineLoadSkipsAbsentConventionalSecretsDirectory(t *testing.T) {
|
|
pipelinePath := filepath.Join(t.TempDir(), "pipeline.yml")
|
|
if err := os.WriteFile(pipelinePath, []byte("version: 1\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
secretCalls := 0
|
|
pipelineCalls := 0
|
|
deps := commandDependencies{
|
|
loadSecretsDir: func(string) (*configkit.SecretsReport, error) {
|
|
secretCalls++
|
|
return nil, nil
|
|
},
|
|
loadPipelineConfig: func(string, config.PipelineLoadOptions) (*config.PipelineConfig, error) {
|
|
pipelineCalls++
|
|
return &config.PipelineConfig{}, nil
|
|
},
|
|
}
|
|
|
|
if _, _, err := loadPipelineConfigWithDependencies(pipelinePath, config.PipelineLoadOptions{}, nil, deps); err != nil {
|
|
t.Fatalf("load pipeline: %v", err)
|
|
}
|
|
if secretCalls != 0 || pipelineCalls != 1 {
|
|
t.Fatalf("calls = secrets %d, pipeline %d; want 0, 1", secretCalls, pipelineCalls)
|
|
}
|
|
}
|
|
|
|
func TestExplicitSecretsDirectoryIsResolvedFromCallerWorkingDirectory(t *testing.T) {
|
|
callerDir := t.TempDir()
|
|
selected := filepath.Join(callerDir, "credentials")
|
|
if err := os.Mkdir(selected, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
original, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Chdir(callerDir); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = os.Chdir(original) })
|
|
|
|
got, required, err := selectedSecretsDirectory(filepath.Join(t.TempDir(), "pipeline.yml"), stringPointer("credentials"))
|
|
if err != nil {
|
|
t.Fatalf("select secrets: %v", err)
|
|
}
|
|
if !required || got != selected {
|
|
t.Fatalf("selection = %q, required=%t; want %q, true", got, required, selected)
|
|
}
|
|
}
|
|
|
|
func TestExplicitSecretsFailurePrecedesPipelineLoad(t *testing.T) {
|
|
pipelinePath := filepath.Join(t.TempDir(), "pipeline.yml")
|
|
if err := os.WriteFile(pipelinePath, []byte("version: 1\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for name, selection := range map[string]string{"empty": "", "missing": filepath.Join(t.TempDir(), "missing")} {
|
|
t.Run(name, func(t *testing.T) {
|
|
pipelineCalls := 0
|
|
deps := commandDependencies{loadPipelineConfig: func(string, config.PipelineLoadOptions) (*config.PipelineConfig, error) {
|
|
pipelineCalls++
|
|
return &config.PipelineConfig{}, nil
|
|
}}
|
|
if _, _, err := loadPipelineConfigWithDependencies(pipelinePath, config.PipelineLoadOptions{}, &selection, deps); err == nil {
|
|
t.Fatal("load succeeded, want bootstrap error")
|
|
}
|
|
if pipelineCalls != 0 {
|
|
t.Fatalf("pipeline load calls = %d, want 0", pipelineCalls)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSecretsBootstrapPreservesExistingEnvironmentValues(t *testing.T) {
|
|
directory := t.TempDir()
|
|
const emptyName = "NARRATIO_BOOTSTRAP_EXISTING_EMPTY"
|
|
const valueName = "NARRATIO_BOOTSTRAP_EXISTING_VALUE"
|
|
const loadedName = "NARRATIO_BOOTSTRAP_NEW_VALUE"
|
|
t.Setenv(emptyName, "")
|
|
t.Setenv(valueName, "from-environment")
|
|
restoreEnvAfterTest(t, loadedName)
|
|
for name, value := range map[string]string{
|
|
emptyName: "from-file-empty\n",
|
|
valueName: "from-file-value\n",
|
|
loadedName: "new-value\n",
|
|
} {
|
|
mustWriteSecretFile(t, filepath.Join(directory, name), value)
|
|
}
|
|
|
|
bootstrap := &secretsBootstrap{selection: &directory, selectionSet: true}
|
|
var diagnostics bytes.Buffer
|
|
if err := bootstrap.load(filepath.Join(t.TempDir(), "pipeline.yml"), configkit.LoadSecretsDir, &diagnostics); err != nil {
|
|
t.Fatalf("bootstrap: %v", err)
|
|
}
|
|
if got := diagnostics.String(); !strings.Contains(got, `status="skipped_existing"`) || !strings.Contains(got, `existing_empty=true`) {
|
|
t.Fatalf("diagnostics lack explicit skipped-empty result: %q", got)
|
|
}
|
|
if got := os.Getenv(emptyName); got != "" {
|
|
t.Fatalf("existing empty value = %q, want empty", got)
|
|
}
|
|
if got := os.Getenv(valueName); got != "from-environment" {
|
|
t.Fatalf("existing value = %q, want environment value", got)
|
|
}
|
|
if got := os.Getenv(loadedName); got != "new-value" {
|
|
t.Fatalf("new value = %q, want file value", got)
|
|
}
|
|
}
|
|
|
|
func TestNotariusCredentialLoadsFromCommandSecretBootstrap(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
explicit bool
|
|
environment string
|
|
fileValue string
|
|
wantValue string
|
|
}{
|
|
{name: "conventional", fileValue: "conventional-value", wantValue: "conventional-value"},
|
|
{name: "explicit", explicit: true, fileValue: "explicit-value", wantValue: "explicit-value"},
|
|
{name: "environment precedence", explicit: true, environment: "environment-value", fileValue: "file-value", wantValue: "environment-value"},
|
|
}
|
|
for index, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
envName := fmt.Sprintf("NARRATIO_NOTARIUS_BOOTSTRAP_%d", index)
|
|
restoreEnvAfterTest(t, envName)
|
|
root := t.TempDir()
|
|
pipelinePath := filepath.Join(root, "pipeline.yml")
|
|
if err := os.WriteFile(pipelinePath, []byte("whisperx:\n transcribe_url: https://transcription.example.test/transcribe\nnotarius:\n enabled: false\n credentials: ["+envName+"]\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
secretsDir := filepath.Join(root, "secrets")
|
|
var selection *string
|
|
if test.explicit {
|
|
secretsDir = filepath.Join(t.TempDir(), "selected-secrets")
|
|
selection = &secretsDir
|
|
}
|
|
mustWriteSecretFile(t, filepath.Join(secretsDir, envName), test.fileValue+"\n")
|
|
if test.environment != "" {
|
|
if err := os.Setenv(envName, test.environment); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
_, pipeline, err := loadPipelineConfigWithDependencies(pipelinePath, config.PipelineLoadOptions{}, selection, commandDependencies{diagnostics: io.Discard})
|
|
if err != nil {
|
|
t.Fatalf("load pipeline: %v", err)
|
|
}
|
|
if pipeline.Notarius == nil || len(pipeline.Notarius.Credentials) != 1 || !pipeline.Notarius.Credentials[0].IsResolved() ||
|
|
pipeline.Notarius.Credentials[0].EnvName() != envName || pipeline.Notarius.Credentials[0].Value() != test.wantValue {
|
|
t.Fatal("Notarius credential did not resolve from the selected command bootstrap source")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSecretsBootstrapPreservesSafePartialReportOnFailure(t *testing.T) {
|
|
directory := t.TempDir()
|
|
mustWriteSecretFile(t, filepath.Join(directory, "INVALID-NAME"), "must-not-appear\n")
|
|
bootstrap := &secretsBootstrap{selection: &directory, selectionSet: true}
|
|
err := bootstrap.load(filepath.Join(t.TempDir(), "pipeline.yml"), configkit.LoadSecretsDir, io.Discard)
|
|
if err == nil {
|
|
t.Fatal("bootstrap succeeded, want error")
|
|
}
|
|
if !strings.Contains(err.Error(), `status="failed"`) || !strings.Contains(err.Error(), `name="INVALID-NAME"`) {
|
|
t.Fatalf("error lacks partial report: %v", err)
|
|
}
|
|
if strings.Contains(err.Error(), "must-not-appear") {
|
|
t.Fatalf("error exposed secret value: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestExecuteConfigShowReportsSuccessfulSecretsBootstrapOnStderr(t *testing.T) {
|
|
workspaceRoot := t.TempDir()
|
|
pipelinePath, _, _ := writeValidConfigFiles(t, workspaceRoot)
|
|
secretsDir := t.TempDir()
|
|
const name = "NARRATIO_BOOTSTRAP_REPORT_SECRET"
|
|
const canary = "synthetic-secret-must-not-appear"
|
|
restoreEnvAfterTest(t, name)
|
|
if err := os.Unsetenv(name); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mustWriteSecretFile(t, filepath.Join(secretsDir, name), canary+"\n")
|
|
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
if code := Execute([]string{"config", "show", "--config", pipelinePath, "--secrets-dir", secretsDir}, &stdout, &stderr); code != 0 {
|
|
t.Fatalf("config show exit = %d, stderr = %q", code, stderr.String())
|
|
}
|
|
if got := stderr.String(); !strings.Contains(got, `status="loaded"`) || !strings.Contains(got, `name="`+name+`"`) {
|
|
t.Fatalf("stderr lacks safe bootstrap report: %q", got)
|
|
}
|
|
if strings.Contains(stderr.String(), canary) || strings.Contains(stdout.String(), canary) {
|
|
t.Fatal("command output exposed secret value")
|
|
}
|
|
if strings.Contains(stdout.String(), `status="loaded"`) || strings.Contains(stdout.String(), name) {
|
|
t.Fatalf("show stdout was polluted by bootstrap report: %q", stdout.String())
|
|
}
|
|
}
|
|
|
|
func TestExecuteConfigValidateReportsSkippedEmptyBeforeTypedLoadFailure(t *testing.T) {
|
|
workspaceRoot := t.TempDir()
|
|
pipelinePath, _, _ := writeValidConfigFiles(t, workspaceRoot)
|
|
const name = "NARRATIO_REVIEW_KEY"
|
|
const canary = "synthetic-skipped-secret-must-not-appear"
|
|
t.Setenv(name, "")
|
|
pipelineYAML, err := os.ReadFile(pipelinePath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pipelineYAML = append(pipelineYAML, []byte("notarius:\n enabled: false\n credentials: ["+name+"]\n")...)
|
|
if err := os.WriteFile(pipelinePath, pipelineYAML, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
secretsDir := t.TempDir()
|
|
mustWriteSecretFile(t, filepath.Join(secretsDir, name), canary+"\n")
|
|
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
if code := Execute([]string{"config", "validate", "--config", pipelinePath, "--secrets-dir", secretsDir}, &stdout, &stderr); code == 0 {
|
|
t.Fatal("config validate succeeded, want empty-secret failure")
|
|
}
|
|
got := stderr.String()
|
|
if !strings.Contains(got, `status="skipped_existing"`) || !strings.Contains(got, `existing_empty=true`) {
|
|
t.Fatalf("stderr lacks useful skipped-empty diagnostic: %q", got)
|
|
}
|
|
if !strings.Contains(got, "secret.empty") {
|
|
t.Fatalf("stderr lacks typed load failure: %q", got)
|
|
}
|
|
if strings.Contains(got, canary) || strings.Contains(stdout.String(), canary) {
|
|
t.Fatal("command output exposed skipped secret value")
|
|
}
|
|
}
|
|
|
|
func stringPointer(value string) *string { return &value }
|