Files
distributor/internal/app/validate_test.go

265 lines
8.2 KiB
Go

package app
import (
"bytes"
"context"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"gitea.maximumdirect.net/eric/distributor/internal/config"
"gitea.maximumdirect.net/eric/distributor/internal/storage"
"gitea.maximumdirect.net/eric/distributor/internal/storage/fake"
"gitea.maximumdirect.net/eric/distributor/internal/testutil"
)
func TestValidateLocalBundle(t *testing.T) {
var stdout bytes.Buffer
err := Validate(context.Background(), ValidateOptions{
Path: filepath.Join("..", "bundle", "testdata", "valid_bundle"),
Stdout: &stdout,
})
if err != nil {
t.Fatalf("Validate() error = %v", err)
}
if got, want := stdout.String(), "Validated 1 bundle(s)\n"; got != want {
t.Fatalf("stdout = %q, want %q", got, want)
}
}
func TestValidateExampleSourceBundle(t *testing.T) {
err := Validate(context.Background(), ValidateOptions{
Path: filepath.Join("..", "..", "examples", "source-bundle"),
})
if err != nil {
t.Fatalf("Validate() example error = %v", err)
}
}
func TestValidateConfiguredLocalSource(t *testing.T) {
sourceRoot := t.TempDir()
destinationRoot := t.TempDir()
testutil.WriteSourceBundle(t, sourceRoot, "", testutil.BundleOptions{})
var stdout bytes.Buffer
err := Validate(context.Background(), ValidateOptions{
ConfigPath: testutil.WriteMinimalLocalConfig(t, sourceRoot, destinationRoot),
PipelineID: "reports",
Stdout: &stdout,
})
if err != nil {
t.Fatalf("Validate() configured source error = %v", err)
}
if got, want := stdout.String(), "Validated 1 bundle(s) for pipeline reports source local\n"; got != want {
t.Fatalf("stdout = %q, want %q", got, want)
}
}
func TestValidateConfiguredSourceBundlePath(t *testing.T) {
sourceRoot := t.TempDir()
destinationRoot := t.TempDir()
testutil.WriteSourceBundle(t, sourceRoot, "daily/one", testutil.BundleOptions{ID: "reports.one"})
testutil.WriteSourceBundle(t, sourceRoot, "daily/two", testutil.BundleOptions{ID: "reports.two"})
var stdout bytes.Buffer
err := Validate(context.Background(), ValidateOptions{
ConfigPath: testutil.WriteMinimalLocalConfig(t, sourceRoot, destinationRoot),
PipelineID: "reports",
BundlePath: "daily/two",
Stdout: &stdout,
OutputFormat: OutputFormatJSON,
})
if err != nil {
t.Fatalf("Validate() configured bundle error = %v", err)
}
result := decodeAppResult(t, stdout.String())
if result["pipeline_id"] != "reports" || result["source_backend"] != "local" || result["bundle_count"] != float64(1) {
t.Fatalf("result = %#v, want configured source summary", result)
}
bundles, ok := result["bundles"].([]any)
if !ok || len(bundles) != 1 {
t.Fatalf("bundles = %#v, want one bundle", result["bundles"])
}
sourceBundle, ok := bundles[0].(map[string]any)
if !ok || sourceBundle["path"] != "daily/two" || sourceBundle["id"] != "reports.two" {
t.Fatalf("bundle = %#v, want narrowed bundle", sourceBundle)
}
}
func TestValidateConfiguredRemoteSourcesThroughStorageAbstraction(t *testing.T) {
s3Source := fake.New()
testutil.WriteFakeSourceBundle(t, s3Source, "", testutil.BundleOptions{ID: "reports.s3"})
sshSource := fake.New()
testutil.WriteFakeSourceBundle(t, sshSource, "daily", testutil.BundleOptions{ID: "reports.ssh"})
cfg := config.Config{Pipelines: []config.Pipeline{
{
ID: "s3-reports",
Source: config.Backend{
Backend: config.BackendS3,
Endpoint: "http://s3.test",
Bucket: "source-bucket",
},
Destinations: []config.Destination{{
ID: "archive",
Backend: config.BackendLocal,
Path: t.TempDir(),
}},
},
{
ID: "ssh-reports",
Source: config.Backend{
Backend: config.BackendSSH,
Host: "ssh.test",
Path: "/source",
},
Destinations: []config.Destination{{
ID: "archive",
Backend: config.BackendLocal,
Path: t.TempDir(),
}},
},
}}
config.ApplyDefaults(&cfg)
provider := fakeBackendFactoryProvider(t, map[string]storage.Backend{
"s3:source-bucket": s3Source,
"ssh:/source": sshSource,
})
var s3Stdout bytes.Buffer
if err := validateConfigWithBackendFactory(context.Background(), cfg, ValidateOptions{
PipelineID: "s3-reports",
Stdout: &s3Stdout,
OutputFormat: OutputFormatJSON,
}, provider); err != nil {
t.Fatalf("validate s3 source error = %v", err)
}
s3Result := decodeAppResult(t, s3Stdout.String())
if s3Result["source_backend"] != "s3" || s3Result["bundle_count"] != float64(1) {
t.Fatalf("s3 result = %#v, want one s3 bundle", s3Result)
}
var sshStdout bytes.Buffer
if err := validateConfigWithBackendFactory(context.Background(), cfg, ValidateOptions{
PipelineID: "ssh-reports",
BundlePath: "daily",
Stdout: &sshStdout,
}, provider); err != nil {
t.Fatalf("validate ssh source error = %v", err)
}
if !strings.Contains(sshStdout.String(), "pipeline ssh-reports source ssh") {
t.Fatalf("ssh stdout = %q, want ssh source summary", sshStdout.String())
}
}
func TestValidateConfiguredSourceLoadsSecretsBeforeOpeningBackend(t *testing.T) {
sourceRoot := filepath.Join(t.TempDir(), "missing-source")
destinationRoot := t.TempDir()
configPath := writeConfigFile(t, `
secrets:
directory: `+filepath.Join(t.TempDir(), "missing-secrets")+`
pipelines:
- id: reports
source:
backend: local
path: `+sourceRoot+`
destinations:
- id: archive
backend: local
path: `+destinationRoot+`
`)
err := Validate(context.Background(), ValidateOptions{ConfigPath: configPath, PipelineID: "reports"})
if err == nil {
t.Fatal("Validate() error = nil, want secrets directory error")
}
if !strings.Contains(err.Error(), "load secrets directory") {
t.Fatalf("Validate() error = %v, want secrets directory error", err)
}
if strings.Contains(err.Error(), "missing-source") {
t.Fatalf("Validate() error = %v, opened source before loading secrets", err)
}
}
func TestValidateConfiguredSourcePrintsSecretConflictWarningWithoutValues(t *testing.T) {
name := "DISTRIBUTOR_TEST_VALIDATE_SECRET"
t.Setenv(name, "process-value")
sourceRoot := t.TempDir()
destinationRoot := t.TempDir()
secretsRoot := t.TempDir()
if err := os.WriteFile(filepath.Join(secretsRoot, name), []byte("secret-value\n"), 0o600); err != nil {
t.Fatalf("write secret: %v", err)
}
testutil.WriteSourceBundle(t, sourceRoot, "", testutil.BundleOptions{})
configPath := writeConfigFile(t, `
secrets:
directory: `+secretsRoot+`
pipelines:
- id: reports
source:
backend: local
path: `+sourceRoot+`
destinations:
- id: archive
backend: local
path: `+destinationRoot+`
`)
var stdout bytes.Buffer
err := Validate(context.Background(), ValidateOptions{
ConfigPath: configPath,
PipelineID: "reports",
Stdout: &stdout,
})
if err != nil {
t.Fatalf("Validate() error = %v", err)
}
output := stdout.String()
if !strings.Contains(output, "secret "+name+" ignored because the real environment already has that variable") {
t.Fatalf("stdout = %q, want secret conflict warning", output)
}
if !strings.Contains(output, "Validated 1 bundle(s) for pipeline reports source local") {
t.Fatalf("stdout = %q, want validate summary", output)
}
if strings.Contains(output, "process-value") || strings.Contains(output, "secret-value") {
t.Fatalf("stdout exposed secret values: %q", output)
}
}
func TestValidateConfiguredSourceRequiresPipeline(t *testing.T) {
sourceRoot := t.TempDir()
destinationRoot := t.TempDir()
testutil.WriteSourceBundle(t, sourceRoot, "", testutil.BundleOptions{})
err := Validate(context.Background(), ValidateOptions{
ConfigPath: testutil.WriteMinimalLocalConfig(t, sourceRoot, destinationRoot),
})
if err == nil || !strings.Contains(err.Error(), "requires --pipeline") {
t.Fatalf("Validate() error = %v, want required pipeline", err)
}
}
func TestValidateRequiresPath(t *testing.T) {
err := Validate(context.Background(), ValidateOptions{})
if err == nil || !strings.Contains(err.Error(), "requires a path") {
t.Fatalf("Validate() error = %v, want required path", err)
}
}
func decodeAppResult(t *testing.T, output string) map[string]any {
t.Helper()
var envelope map[string]any
if err := json.Unmarshal([]byte(output), &envelope); err != nil {
t.Fatalf("decode output: %v; output = %q", err, output)
}
result, ok := envelope["result"].(map[string]any)
if !ok {
t.Fatalf("result = %#v, want object", envelope["result"])
}
return result
}