Files
distributor/internal/cli/root_test.go

287 lines
7.9 KiB
Go

package cli
import (
"bytes"
"context"
"os"
"path/filepath"
"strings"
"testing"
"gitea.maximumdirect.net/eric/distributor/internal/storage"
)
func TestExecuteRootHelp(t *testing.T) {
var stdout, stderr bytes.Buffer
code := Execute(context.Background(), []string{"--help"}, &stdout, &stderr)
if code != exitOK {
t.Fatalf("exit code = %d, want %d", code, exitOK)
}
if !strings.Contains(stdout.String(), "Usage:") {
t.Fatalf("stdout = %q, want help text", stdout.String())
}
if stderr.Len() != 0 {
t.Fatalf("stderr = %q, want empty", stderr.String())
}
}
func TestExecuteVersion(t *testing.T) {
var stdout, stderr bytes.Buffer
code := Execute(context.Background(), []string{"version"}, &stdout, &stderr)
if code != exitOK {
t.Fatalf("exit code = %d, want %d", code, exitOK)
}
if got, want := stdout.String(), "distributor dev\n"; got != want {
t.Fatalf("stdout = %q, want %q", got, want)
}
if stderr.Len() != 0 {
t.Fatalf("stderr = %q, want empty", stderr.String())
}
}
func TestExecuteValidate(t *testing.T) {
var stdout, stderr bytes.Buffer
code := Execute(context.Background(), []string{"validate", filepath.Join("..", "bundle", "testdata", "valid_bundle")}, &stdout, &stderr)
if code != exitOK {
t.Fatalf("exit code = %d, want %d; stderr = %q", code, exitOK, stderr.String())
}
if got, want := stdout.String(), "Validated 1 bundle(s)\n"; got != want {
t.Fatalf("stdout = %q, want %q", got, want)
}
}
func TestExecuteValidateArgs(t *testing.T) {
validPath := filepath.Join("..", "bundle", "testdata", "valid_bundle")
tests := []struct {
name string
args []string
wantCode int
wantStdout string
wantStderr string
}{
{
name: "zero args",
args: []string{"validate"},
wantCode: exitError,
wantStderr: "requires a path",
},
{
name: "one arg",
args: []string{"validate", validPath},
wantCode: exitOK,
wantStdout: "Validated 1 bundle(s)",
},
{
name: "two args",
args: []string{"validate", validPath, validPath},
wantCode: exitUsage,
wantStderr: "accepts at most one path",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var stdout, stderr bytes.Buffer
code := Execute(context.Background(), tt.args, &stdout, &stderr)
if code != tt.wantCode {
t.Fatalf("exit code = %d, want %d; stderr = %q", code, tt.wantCode, stderr.String())
}
if tt.wantStdout != "" && !strings.Contains(stdout.String(), tt.wantStdout) {
t.Fatalf("stdout = %q, want substring %q", stdout.String(), tt.wantStdout)
}
if tt.wantStderr != "" && !strings.Contains(stderr.String(), tt.wantStderr) {
t.Fatalf("stderr = %q, want substring %q", stderr.String(), tt.wantStderr)
}
})
}
}
func TestExecuteInspect(t *testing.T) {
var stdout, stderr bytes.Buffer
code := Execute(context.Background(), []string{"inspect", filepath.Join("..", "bundle", "testdata", "valid_bundle")}, &stdout, &stderr)
if code != exitOK {
t.Fatalf("exit code = %d, want %d; stderr = %q", code, exitOK, stderr.String())
}
if !strings.Contains(stdout.String(), "id=weather.daily.brentwood.2026-05-30") {
t.Fatalf("stdout = %q, want bundle summary", stdout.String())
}
}
func TestExecuteInspectArgs(t *testing.T) {
validPath := filepath.Join("..", "bundle", "testdata", "valid_bundle")
tests := []struct {
name string
args []string
wantCode int
wantStdout string
wantStderr string
}{
{
name: "zero args",
args: []string{"inspect"},
wantCode: exitError,
wantStderr: "requires a path",
},
{
name: "one arg",
args: []string{"inspect", validPath},
wantCode: exitOK,
wantStdout: "id=weather.daily.brentwood.2026-05-30",
},
{
name: "two args",
args: []string{"inspect", validPath, validPath},
wantCode: exitUsage,
wantStderr: "accepts at most one path",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var stdout, stderr bytes.Buffer
code := Execute(context.Background(), tt.args, &stdout, &stderr)
if code != tt.wantCode {
t.Fatalf("exit code = %d, want %d; stderr = %q", code, tt.wantCode, stderr.String())
}
if tt.wantStdout != "" && !strings.Contains(stdout.String(), tt.wantStdout) {
t.Fatalf("stdout = %q, want substring %q", stdout.String(), tt.wantStdout)
}
if tt.wantStderr != "" && !strings.Contains(stderr.String(), tt.wantStderr) {
t.Fatalf("stderr = %q, want substring %q", stderr.String(), tt.wantStderr)
}
})
}
}
func TestExecuteRunDryRun(t *testing.T) {
sourceRoot := t.TempDir()
writeCLIBundle(t, sourceRoot)
configPath := filepath.Join(t.TempDir(), "config.yml")
err := os.WriteFile(configPath, []byte(`
pipelines:
- id: reports
source:
backend: local
path: `+sourceRoot+`
destinations:
- id: archive
backend: local
path: `+t.TempDir()+`
`), 0o600)
if err != nil {
t.Fatalf("write config: %v", err)
}
var stdout, stderr bytes.Buffer
code := Execute(context.Background(), []string{"run", "--config", configPath, "--dry-run"}, &stdout, &stderr)
if code != exitOK {
t.Fatalf("exit code = %d, want %d; stderr = %q", code, exitOK, stderr.String())
}
if !strings.Contains(stdout.String(), "action=publish_new") {
t.Fatalf("stdout = %q, want config summary", stdout.String())
}
if stderr.Len() != 0 {
t.Fatalf("stderr = %q, want empty", stderr.String())
}
}
func TestExecuteRunRejectsExtraPositionalArgs(t *testing.T) {
var stdout, stderr bytes.Buffer
code := Execute(context.Background(), []string{"run", "--config", "config.yml", "extra"}, &stdout, &stderr)
if code != exitUsage {
t.Fatalf("exit code = %d, want %d", code, exitUsage)
}
if !strings.Contains(stderr.String(), "does not accept positional arguments") {
t.Fatalf("stderr = %q, want positional argument error", stderr.String())
}
}
func TestExecuteRunPublishes(t *testing.T) {
sourceRoot := t.TempDir()
destinationRoot := t.TempDir()
writeCLIBundle(t, sourceRoot)
configPath := filepath.Join(t.TempDir(), "config.yml")
err := os.WriteFile(configPath, []byte(`
pipelines:
- id: reports
source:
backend: local
path: `+sourceRoot+`
destinations:
- id: archive
backend: local
path: `+destinationRoot+`
`), 0o600)
if err != nil {
t.Fatalf("write config: %v", err)
}
var stdout, stderr bytes.Buffer
code := Execute(context.Background(), []string{"run", "--config", configPath}, &stdout, &stderr)
if code != exitOK {
t.Fatalf("exit code = %d, want %d; stderr = %q", code, exitOK, stderr.String())
}
if _, err := os.Stat(filepath.Join(destinationRoot, storage.StateFileName)); err != nil {
t.Fatalf("state stat error = %v", err)
}
}
func TestUnknownCommandIsUsageError(t *testing.T) {
var stdout, stderr bytes.Buffer
code := Execute(context.Background(), []string{"nope"}, &stdout, &stderr)
if code != exitUsage {
t.Fatalf("exit code = %d, want %d", code, exitUsage)
}
if !strings.Contains(stderr.String(), "unknown command") {
t.Fatalf("stderr = %q, want unknown command error", stderr.String())
}
}
func writeCLIBundle(t *testing.T, root string) {
t.Helper()
for _, file := range []struct {
path string
data string
}{
{"manifest.json", `{
"schema_version": 1,
"id": "weather.daily.brentwood.2026-05-30",
"digest": "sha256:099b205780d2b050024868399961b05731729a548d5d6329c7b06a6740dd75fe",
"created": "2026-05-30T11:10:00Z",
"files": [
{
"path": "report.md",
"sha256": "sha256:3640fd37140ee4d2e0e93e78834f232ea67a50e7bc6279203690cc7de1975fa6",
"size": 16
},
{
"path": "summary.txt",
"sha256": "sha256:3cbb36aca330b3bd113955dfbada0adb7a5f95ad9f678bd61f175406c6a37e95",
"size": 8
}
]
}
`},
{"report.md", "# Report\nSunny.\n"},
{"summary.txt", "Summary\n"},
} {
if err := os.WriteFile(filepath.Join(root, file.path), []byte(file.data), 0o600); err != nil {
t.Fatalf("write bundle file: %v", err)
}
}
}