530 lines
20 KiB
Go
530 lines
20 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/aritmolab/thothii/tools/thothctl/internal/testsupport"
|
|
)
|
|
|
|
func TestRunLogsRedactsAnUnlabelledDeclaredSecret(t *testing.T) {
|
|
fixture := newCLIFixture(t, "UNLABELLED_SECRET_FILE=%s\n")
|
|
secretPath := filepath.Join(fixture.root, "operator-secret")
|
|
if err := os.WriteFile(secretPath, []byte("unlabelled-secret\r\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fixture.setEnvironment(t, secretPath)
|
|
t.Setenv("THOTHCTL_FAKE_LOG", "fake Docker log: unlabelled-secret")
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "logs"}, &stdout, &stderr)
|
|
|
|
if exitCode != 0 {
|
|
t.Fatalf("run() exit code = %d, stderr = %s", exitCode, stderr.String())
|
|
}
|
|
if strings.Contains(stdout.String(), "unlabelled-secret") {
|
|
t.Fatalf("logs exposed an unlabelled secret: %q", stdout.String())
|
|
}
|
|
if stdout.String() != "fake Docker log: [REDACTED]\n" {
|
|
t.Errorf("logs = %q, want redacted output", stdout.String())
|
|
}
|
|
}
|
|
|
|
func TestRunResolvesComposeDotenvCommentsQuotesAndInterpolationForSecretFiles(t *testing.T) {
|
|
fixture := newCLIFixture(t, "")
|
|
secretDirectory := filepath.Join(fixture.root, "secret directory")
|
|
if err := os.Mkdir(secretDirectory, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
inlineSecret := filepath.Join(secretDirectory, "inline")
|
|
doubleQuotedSecret := filepath.Join(secretDirectory, "double quoted")
|
|
singleQuotedSecret := filepath.Join(secretDirectory, "single quoted")
|
|
interpolatedSecret := filepath.Join(secretDirectory, "interpolated")
|
|
for path, value := range map[string]string{
|
|
inlineSecret: "inline-secret",
|
|
doubleQuotedSecret: "double-quoted-secret",
|
|
singleQuotedSecret: "single-quoted-secret",
|
|
interpolatedSecret: "interpolated-secret",
|
|
} {
|
|
if err := os.WriteFile(path, []byte(value), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
fixture.setEnvContents(t, "SECRET_ROOT="+secretDirectory+"\n"+
|
|
"INLINE_TOKEN_FILE="+inlineSecret+" # Compose comment\n"+
|
|
"DOUBLE_TOKEN_FILE=\""+doubleQuotedSecret+"\" # Compose comment\n"+
|
|
"SINGLE_TOKEN_FILE='"+singleQuotedSecret+"' # Compose comment\n"+
|
|
"INTERPOLATED_TOKEN_SOURCE=\"${SECRET_ROOT}/interpolated\"\n")
|
|
t.Setenv("THOTHCTL_FAKE_LOG", "inline-secret double-quoted-secret single-quoted-secret interpolated-secret")
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "logs"}, &stdout, &stderr)
|
|
|
|
if exitCode != 0 {
|
|
t.Fatalf("run() exit code = %d, stderr = %s", exitCode, stderr.String())
|
|
}
|
|
for _, secret := range []string{"inline-secret", "double-quoted-secret", "single-quoted-secret", "interpolated-secret"} {
|
|
if strings.Contains(stdout.String(), secret) {
|
|
t.Errorf("logs exposed %q: %q", secret, stdout.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRunRedactsSecretSourceInBothStreams(t *testing.T) {
|
|
fixture := newCLIFixture(t, "")
|
|
secretPath := filepath.Join(fixture.root, "source-secret")
|
|
if err := os.WriteFile(secretPath, []byte("source-secret"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fixture.setEnvContents(t, "UNLABELLED_SECRET_SOURCE="+secretPath+"\n")
|
|
t.Setenv("THOTHCTL_FAKE_LOG", "stdout source-secret")
|
|
t.Setenv("THOTHCTL_FAKE_FAILURE", "stderr source-secret")
|
|
t.Setenv("THOTHCTL_FAKE_EXIT", "17")
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "logs"}, &stdout, &stderr)
|
|
|
|
if exitCode != 17 {
|
|
t.Errorf("run() exit code = %d, want 17", exitCode)
|
|
}
|
|
if strings.Contains(stdout.String()+stderr.String(), "source-secret") {
|
|
t.Errorf("output exposed source secret: stdout=%q stderr=%q", stdout.String(), stderr.String())
|
|
}
|
|
}
|
|
|
|
func TestRunRedactsSecretWhenDoctorFails(t *testing.T) {
|
|
fixture := newCLIFixture(t, "")
|
|
secretPath := filepath.Join(fixture.root, "doctor-secret")
|
|
if err := os.WriteFile(secretPath, []byte("doctor-secret"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fixture.setEnvContents(t, "DOCTOR_SECRET_FILE="+secretPath+"\n")
|
|
t.Setenv("THOTHCTL_FAKE_FAIL_ON", "version")
|
|
t.Setenv("THOTHCTL_FAKE_FAILURE", "doctor saw doctor-secret")
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "doctor"}, &stdout, &stderr)
|
|
|
|
if exitCode != 41 {
|
|
t.Errorf("run() exit code = %d, want 41", exitCode)
|
|
}
|
|
if strings.Contains(stdout.String()+stderr.String(), "doctor-secret") {
|
|
t.Errorf("doctor failure exposed secret: stdout=%q stderr=%q", stdout.String(), stderr.String())
|
|
}
|
|
}
|
|
|
|
func TestRunFailsClosedForUnresolvedSecretSourceInterpolation(t *testing.T) {
|
|
fixture := newCLIFixture(t, "MISSING_TOKEN_SOURCE=${MISSING_SECRET_ROOT}/token\n")
|
|
fixture.setEnvironment(t)
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "logs"}, &stdout, &stderr)
|
|
|
|
if exitCode != 2 {
|
|
t.Errorf("run() exit code = %d, want 2", exitCode)
|
|
}
|
|
if !strings.Contains(stderr.String(), "installation secret declarations could not be read") {
|
|
t.Errorf("stderr = %q, want fail-closed declaration error", stderr.String())
|
|
}
|
|
if _, err := os.Stat(fixture.argsFile); !os.IsNotExist(err) {
|
|
t.Errorf("Docker was invoked after unresolved interpolation: stat error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRunFailsClosedForTraversalAndParentSymlinkSecretSources(t *testing.T) {
|
|
for name, source := range map[string]func(*testing.T, cliFixture) string{
|
|
"traversal": func(t *testing.T, fixture cliFixture) string {
|
|
secret := filepath.Join(fixture.root, "secret")
|
|
if err := os.WriteFile(secret, []byte("traversal-secret"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return filepath.Join(fixture.root, "subdirectory") + string(filepath.Separator) + ".." + string(filepath.Separator) + "secret"
|
|
},
|
|
"parent symlink": func(t *testing.T, fixture cliFixture) string {
|
|
realDirectory := filepath.Join(fixture.root, "real")
|
|
if err := os.Mkdir(realDirectory, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(realDirectory, "secret"), []byte("symlink-secret"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
linkDirectory := filepath.Join(fixture.root, "linked")
|
|
testsupport.SymlinkOrSkip(t, realDirectory, linkDirectory)
|
|
return filepath.Join(linkDirectory, "secret")
|
|
},
|
|
"final symlink": func(t *testing.T, fixture cliFixture) string {
|
|
realSecret := filepath.Join(fixture.root, "real-secret")
|
|
if err := os.WriteFile(realSecret, []byte("final-symlink-secret"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
linkSecret := filepath.Join(fixture.root, "linked-secret")
|
|
testsupport.SymlinkOrSkip(t, realSecret, linkSecret)
|
|
return linkSecret
|
|
},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
fixture := newCLIFixture(t, "")
|
|
unsafeSource := source(t, fixture)
|
|
fixture.setEnvContents(t, "UNSAFE_SECRET_SOURCE="+unsafeSource+"\n")
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "logs"}, &stdout, &stderr)
|
|
|
|
if exitCode != 2 {
|
|
t.Errorf("run() exit code = %d, want 2", exitCode)
|
|
}
|
|
if !strings.Contains(stderr.String(), "could not be read") {
|
|
t.Errorf("stderr = %q, want sanitized unsafe-file error", stderr.String())
|
|
}
|
|
if strings.Contains(stderr.String(), unsafeSource) {
|
|
t.Errorf("stderr revealed unsafe source path: %q", stderr.String())
|
|
}
|
|
assertDockerNotInvoked(t, fixture)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRunFailsClosedForOversizedEnvAndSecretFiles(t *testing.T) {
|
|
t.Run("environment", func(t *testing.T) {
|
|
fixture := newCLIFixture(t, "")
|
|
fixture.setEnvContents(t, strings.Repeat("A", 1<<20+1))
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "logs"}, &stdout, &stderr)
|
|
if exitCode != 2 || !strings.Contains(stderr.String(), "installation secret declarations could not be read") {
|
|
t.Errorf("exit=%d stderr=%q, want sanitized oversized-env failure", exitCode, stderr.String())
|
|
}
|
|
assertDockerNotInvoked(t, fixture)
|
|
})
|
|
t.Run("secret", func(t *testing.T) {
|
|
fixture := newCLIFixture(t, "")
|
|
secretPath := filepath.Join(fixture.root, "large-secret")
|
|
if err := os.WriteFile(secretPath, make([]byte, 64*1024+1), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fixture.setEnvContents(t, "LARGE_SECRET_FILE="+secretPath+"\n")
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "logs"}, &stdout, &stderr)
|
|
if exitCode != 2 || !strings.Contains(stderr.String(), "declared secret file could not be read") {
|
|
t.Errorf("exit=%d stderr=%q, want sanitized oversized-secret failure", exitCode, stderr.String())
|
|
}
|
|
assertDockerNotInvoked(t, fixture)
|
|
})
|
|
}
|
|
|
|
func TestRunFailsClosedForTooManyOrTooLargeSecretSources(t *testing.T) {
|
|
t.Run("too many sources", func(t *testing.T) {
|
|
fixture := newCLIFixture(t, "")
|
|
var declarations strings.Builder
|
|
for index := range 33 {
|
|
secretPath := filepath.Join(fixture.root, "secret-count-"+strconv.Itoa(index))
|
|
if err := os.WriteFile(secretPath, []byte("secret"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fmt.Fprintf(&declarations, "SECRET_%d_FILE=%s\n", index, secretPath)
|
|
}
|
|
fixture.setEnvContents(t, declarations.String())
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "logs"}, &stdout, &stderr)
|
|
if exitCode != 2 || !strings.Contains(stderr.String(), "installation secret declarations could not be read") {
|
|
t.Errorf("exit=%d stderr=%q, want sanitized source-count failure", exitCode, stderr.String())
|
|
}
|
|
assertDockerNotInvoked(t, fixture)
|
|
})
|
|
|
|
t.Run("total source bytes", func(t *testing.T) {
|
|
fixture := newCLIFixture(t, "")
|
|
var declarations strings.Builder
|
|
for index := range 5 {
|
|
secretPath := filepath.Join(fixture.root, "secret-total-"+strconv.Itoa(index))
|
|
if err := os.WriteFile(secretPath, bytes.Repeat([]byte("x"), 60*1024), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fmt.Fprintf(&declarations, "SECRET_%d_SOURCE=%s\n", index, secretPath)
|
|
}
|
|
fixture.setEnvContents(t, declarations.String())
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "logs"}, &stdout, &stderr)
|
|
if exitCode != 2 || !strings.Contains(stderr.String(), "declared secret file could not be read") {
|
|
t.Errorf("exit=%d stderr=%q, want sanitized total-size failure", exitCode, stderr.String())
|
|
}
|
|
assertDockerNotInvoked(t, fixture)
|
|
})
|
|
}
|
|
|
|
func TestRunStatusUsesStableComposeArguments(t *testing.T) {
|
|
fixture := newCLIFixture(t, "SAFE_VALUE=1\n")
|
|
fixture.setEnvironment(t)
|
|
|
|
for range 2 {
|
|
var stdout, stderr bytes.Buffer
|
|
if exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "status"}, &stdout, &stderr); exitCode != 0 {
|
|
t.Fatalf("run() exit code = %d, stderr = %s", exitCode, stderr.String())
|
|
}
|
|
}
|
|
|
|
invocations := fixture.invocations(t)
|
|
if len(invocations) != 2 {
|
|
t.Fatalf("docker invocations = %d, want 2", len(invocations))
|
|
}
|
|
if strings.Join(invocations[0], "\x00") != strings.Join(invocations[1], "\x00") {
|
|
t.Errorf("Compose arguments changed between identical status calls: %#v then %#v", invocations[0], invocations[1])
|
|
}
|
|
wantSuffix := []string{
|
|
"--project-directory", fixture.projectDirectory,
|
|
"--env-file", fixture.envFile,
|
|
"-f", filepath.Join(fixture.projectDirectory, "compose.yaml"),
|
|
"-f", filepath.Join(fixture.projectDirectory, "deploy", "compose.local.yaml"),
|
|
"ps", "--format", "json",
|
|
}
|
|
got := invocations[0]
|
|
if len(got) != len(wantSuffix)+3 || got[0] != "compose" || got[1] != "--project-name" || !strings.HasPrefix(got[2], "thothii-") {
|
|
t.Fatalf("unexpected Compose prefix: %#v", got)
|
|
}
|
|
for index, want := range wantSuffix {
|
|
if got[index+3] != want {
|
|
t.Errorf("argument %d = %q, want %q", index+3, got[index+3], want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRunExplainsWhenDockerIsNotAvailable(t *testing.T) {
|
|
fixture := newCLIFixture(t, "SAFE_VALUE=1\n")
|
|
fixture.setEnvironment(t)
|
|
t.Setenv("PATH", t.TempDir())
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "status"}, &stdout, &stderr)
|
|
|
|
if exitCode != 127 {
|
|
t.Errorf("run() exit code = %d, want 127", exitCode)
|
|
}
|
|
if !strings.Contains(stderr.String(), "Docker is not installed or is not on PATH") {
|
|
t.Errorf("stderr = %q, want Docker-not-found guidance", stderr.String())
|
|
}
|
|
if strings.Contains(stderr.String(), "executable file") {
|
|
t.Errorf("stderr leaked a process implementation detail: %q", stderr.String())
|
|
}
|
|
}
|
|
|
|
func TestRunDoctorValidatesTheRenderedInstallation(t *testing.T) {
|
|
fixture := newCLIFixture(t, "SAFE_VALUE=1\n")
|
|
fixture.setEnvironment(t)
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "doctor"}, &stdout, &stderr)
|
|
|
|
if exitCode != 0 {
|
|
t.Fatalf("run() exit code = %d, stderr = %s", exitCode, stderr.String())
|
|
}
|
|
if stdout.String() != "Doctor checks passed.\n" {
|
|
t.Errorf("stdout = %q, want doctor success", stdout.String())
|
|
}
|
|
}
|
|
|
|
func TestRunPreservesChildExitCodes(t *testing.T) {
|
|
fixture := newCLIFixture(t, "SAFE_VALUE=1\n")
|
|
fixture.setEnvironment(t)
|
|
t.Setenv("THOTHCTL_FAKE_EXIT", "42")
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "update", "--check-only"}, &stdout, &stderr)
|
|
|
|
if exitCode != 42 {
|
|
t.Errorf("run() exit code = %d, want 42", exitCode)
|
|
}
|
|
if stderr.String() != "fake Docker failure\n" {
|
|
t.Errorf("stderr = %q, want child stderr", stderr.String())
|
|
}
|
|
}
|
|
|
|
func TestRunPiStatusUsesImageBundledPi(t *testing.T) {
|
|
fixture := newCLIFixture(t, "THT_LLM_URL=https://llm.example.invalid\n")
|
|
fixture.setEnvironment(t)
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "pi", "status"}, &stdout, &stderr)
|
|
|
|
if exitCode != 0 {
|
|
t.Fatalf("run() exit code = %d, stderr = %s", exitCode, stderr.String())
|
|
}
|
|
if stdout.String() != "Pi version: 0.80.3\n" {
|
|
t.Errorf("stdout = %q, want image-bundled Pi version", stdout.String())
|
|
}
|
|
assertInvocationContains(t, fixture.invocations(t), "exec", "-T", "core", "pi", "--version")
|
|
}
|
|
|
|
func TestRunPiUpdateRequiresExplicitConfirmationWithoutInvokingDocker(t *testing.T) {
|
|
fixture := newCLIFixture(t, "THT_LLM_URL=https://llm.example.invalid\n")
|
|
fixture.setEnvironment(t)
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "pi", "update", "--version", "0.81.0"}, &stdout, &stderr)
|
|
|
|
if exitCode != 2 {
|
|
t.Errorf("run() exit code = %d, want 2", exitCode)
|
|
}
|
|
if !strings.Contains(stderr.String(), "requires --yes") {
|
|
t.Errorf("stderr = %q, want explicit confirmation guidance", stderr.String())
|
|
}
|
|
assertDockerNotInvoked(t, fixture)
|
|
}
|
|
|
|
func TestRunPiStatusPreservesDockerExitCodeAndRedactsDiagnostics(t *testing.T) {
|
|
fixture := newCLIFixture(t, "PI_TOKEN_FILE=%s\n")
|
|
secretPath := filepath.Join(fixture.root, "pi-secret")
|
|
if err := os.WriteFile(secretPath, []byte("pi-status-secret"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fixture.setEnvironment(t, secretPath)
|
|
t.Setenv("THOTHCTL_FAKE_FAIL_ON", "version")
|
|
t.Setenv("THOTHCTL_FAKE_FAILURE", "pi-status-secret")
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "pi", "status"}, &stdout, &stderr)
|
|
|
|
if exitCode != 41 {
|
|
t.Errorf("run() exit code = %d, want 41", exitCode)
|
|
}
|
|
if strings.Contains(stdout.String()+stderr.String(), "pi-status-secret") {
|
|
t.Errorf("Pi status exposed a secret: stdout=%q stderr=%q", stdout.String(), stderr.String())
|
|
}
|
|
}
|
|
|
|
type cliFixture struct {
|
|
root string
|
|
installationPath string
|
|
projectDirectory string
|
|
envFile string
|
|
argsFile string
|
|
pathDirectory string
|
|
envTemplate string
|
|
}
|
|
|
|
func newCLIFixture(t *testing.T, envTemplate string) cliFixture {
|
|
t.Helper()
|
|
temporaryRoot, err := filepath.EvalSymlinks(os.TempDir())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
root, err := os.MkdirTemp(temporaryRoot, "thothctl-test-")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = os.RemoveAll(root) })
|
|
projectDirectory := filepath.Join(root, "project")
|
|
if err := os.MkdirAll(filepath.Join(projectDirectory, "deploy"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, path := range []string{filepath.Join(projectDirectory, "compose.yaml"), filepath.Join(projectDirectory, "deploy", "compose.local.yaml")} {
|
|
if err := os.WriteFile(path, []byte("services: {}\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
envFile := filepath.Join(root, "installation.env")
|
|
installationPath := filepath.Join(root, "thothii-installation.yaml")
|
|
contents := "profile: local\nprojectDirectory: " + projectDirectory + "\nenvFile: " + envFile + "\n"
|
|
if err := os.WriteFile(installationPath, []byte(contents), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pathDirectory := filepath.Join(root, "bin")
|
|
if err := os.Mkdir(pathDirectory, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
argsFile := filepath.Join(root, "docker-args")
|
|
fakeDocker := `#!/bin/sh
|
|
printf '%s\n' "$@" >> "$THOTHCTL_FAKE_ARGS"
|
|
printf '%s\n' -- >> "$THOTHCTL_FAKE_ARGS"
|
|
case " $* " in
|
|
*" config --format json "*) printf '%s\n' '{"volumes":{"settings":{}},"services":{"core":{"image":"thothii-core:local","environment":{"THT_LLM_URL":"https://llm.example.invalid"}}}}' ;;
|
|
*" ps --format json "*) printf '%s\n' '[{"Service":"core","State":"running","Health":"healthy"},{"Service":"frontend","State":"running","Health":"healthy"}]' ;;
|
|
*" pi --version "*) printf '%s\n' '0.80.3' ;;
|
|
*" logs "*) printf '%s\n' "$THOTHCTL_FAKE_LOG" ;;
|
|
esac
|
|
if [ "${THOTHCTL_FAKE_FAIL_ON:-}" = "version" ]; then
|
|
printf '%s\n' "${THOTHCTL_FAKE_FAILURE:-fake Docker failure}" >&2
|
|
exit 41
|
|
fi
|
|
if [ "${THOTHCTL_FAKE_EXIT:-0}" -ne 0 ]; then
|
|
printf '%s\n' "${THOTHCTL_FAKE_FAILURE:-fake Docker failure}" >&2
|
|
fi
|
|
exit "${THOTHCTL_FAKE_EXIT:-0}"
|
|
`
|
|
if err := os.WriteFile(filepath.Join(pathDirectory, "docker"), []byte(fakeDocker), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return cliFixture{root: root, installationPath: installationPath, projectDirectory: projectDirectory, envFile: envFile, argsFile: argsFile, pathDirectory: pathDirectory, envTemplate: envTemplate}
|
|
}
|
|
|
|
func (f cliFixture) setEnvironment(t *testing.T, values ...string) {
|
|
t.Helper()
|
|
env := f.envTemplate
|
|
if len(values) > 0 {
|
|
env = strings.Replace(env, "%s", values[0], 1)
|
|
}
|
|
f.setEnvContents(t, env)
|
|
}
|
|
|
|
func (f cliFixture) setEnvContents(t *testing.T, env string) {
|
|
t.Helper()
|
|
if err := os.WriteFile(f.envFile, []byte(env), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("PATH", f.pathDirectory)
|
|
t.Setenv("THOTHCTL_FAKE_ARGS", f.argsFile)
|
|
t.Setenv("THOTHCTL_FAKE_EXIT", "0")
|
|
t.Setenv("THOTHCTL_FAKE_LOG", "")
|
|
t.Setenv("THOTHCTL_FAKE_FAILURE", "")
|
|
t.Setenv("THOTHCTL_FAKE_FAIL_ON", "")
|
|
}
|
|
|
|
func (f cliFixture) invocations(t *testing.T) [][]string {
|
|
t.Helper()
|
|
contents, err := os.ReadFile(f.argsFile)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var invocations [][]string
|
|
var invocation []string
|
|
for _, line := range strings.Split(strings.TrimSuffix(string(contents), "\n"), "\n") {
|
|
if line == "--" {
|
|
invocations = append(invocations, invocation)
|
|
invocation = nil
|
|
continue
|
|
}
|
|
invocation = append(invocation, line)
|
|
}
|
|
return invocations
|
|
}
|
|
|
|
func assertDockerNotInvoked(t *testing.T, fixture cliFixture) {
|
|
t.Helper()
|
|
if _, err := os.Stat(fixture.argsFile); !os.IsNotExist(err) {
|
|
t.Errorf("Docker was invoked: stat error = %v", err)
|
|
}
|
|
}
|
|
|
|
func assertInvocationContains(t *testing.T, invocations [][]string, want ...string) {
|
|
t.Helper()
|
|
for _, invocation := range invocations {
|
|
for start := range invocation {
|
|
if len(invocation)-start < len(want) {
|
|
continue
|
|
}
|
|
if strings.Join(invocation[start:start+len(want)], "\x00") == strings.Join(want, "\x00") {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
t.Fatalf("invocations = %#v, want %#v", invocations, want)
|
|
}
|