Files
RustProxy/docker-agent/cli_test.go
T
2026-10-19 22:38:17 +02:00

182 lines
6.0 KiB
Go

package main
import (
"bytes"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
)
func TestCLIProvisionShowRedactAndRotate(t *testing.T) {
dir := t.TempDir()
env := filepath.Join(dir, "docker-agent.env")
cert := filepath.Join(dir, "state", "agent.crt")
key := filepath.Join(dir, "state", "agent.key")
initial := "# keep comment\nRUSTPROXY_DOCKER_AGENT_LISTEN=0.0.0.0:19876\n" +
"RUSTPROXY_DOCKER_AGENT_TOKEN=replace-with-at-least-24-random-characters\n" +
"RUSTPROXY_DOCKER_AGENT_HOSTNAME=docker.lab\n"
if err := os.WriteFile(env, []byte(initial), 0600); err != nil {
t.Fatal(err)
}
call := func(command string, flags ...string) string {
t.Helper()
var stdout, stderr bytes.Buffer
args := append([]string{command, "--env-file", env, "--cert-file", cert, "--key-file", key}, flags...)
if err := run(args, &stdout, &stderr); err != nil {
t.Fatalf("run(%s): %v (stderr: %s)", command, err, stderr.String())
}
return stdout.String()
}
output := call("init")
if !strings.Contains(output, "Certificate SHA-256:") {
t.Fatalf("init output %q", output)
}
token := strings.TrimSpace(call("show-token"))
if len(token) != 64 {
t.Fatalf("unexpected token length %d", len(token))
}
if strings.TrimSpace(call("show-token")) != token {
t.Fatal("token changed on read")
}
if !strings.Contains(call("info"), "Token redacted") || strings.Contains(call("info"), token) {
t.Fatal("info leaked the bearer token")
}
if !strings.Contains(call("info", "--show-token"), token) {
t.Fatal("explicit info did not show token")
}
if !strings.Contains(call("init"), "Provisioning complete") {
t.Fatal("second init failed")
}
if strings.TrimSpace(call("show-token")) != token {
t.Fatal("token changed on repeat init")
}
oldFingerprint, err := certificateFingerprint(cert)
if err != nil {
t.Fatal(err)
}
if oldFingerprint != strings.TrimSpace(call("print-fingerprint")) {
t.Fatal("fingerprint changed")
}
if mode, err := os.Stat(key); err != nil || mode.Mode().Perm() != 0600 {
t.Fatalf("key mode: %v, %v", mode, err)
}
if mode, err := os.Stat(env); err != nil || mode.Mode().Perm() != 0600 {
t.Fatalf("env mode: %v, %v", mode, err)
}
if !strings.Contains(call("rotate-token"), "Restart") {
t.Fatal("missing rotation warning")
}
rotated := strings.TrimSpace(call("show-token"))
if len(rotated) != 64 || rotated == token {
t.Fatal("rotation did not change token")
}
raw, err := os.ReadFile(env)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(raw), "# keep comment") || strings.Count(string(raw), "RUSTPROXY_DOCKER_AGENT_TOKEN=") != 1 {
t.Fatalf("rotation changed unrelated config or duplicated tokens: %s", raw)
}
if fp, err := certificateFingerprint(cert); err != nil || fp != oldFingerprint {
t.Fatal("rotation changed certificate")
}
var info agentDetails
if err := json.Unmarshal([]byte(call("info", "--json")), &info); err != nil {
t.Fatal(err)
}
if info.Token != "" || !info.TokenConfigured || info.Certificate == nil || info.Certificate.SHA256 != oldFingerprint || info.Listen != "0.0.0.0:19876" {
t.Fatalf("incorrect json info: %+v", info)
}
if info.Certificate.Subject == "" || info.Certificate.Issuer == "" || info.Certificate.NotAfter == "" || !info.Certificate.ValidNow {
t.Fatalf("missing certificate details: %+v", info.Certificate)
}
}
func TestCLIHelpNeverLeaksToken(t *testing.T) {
dir := t.TempDir()
env := filepath.Join(dir, "agent.env")
secret := strings.Repeat("very-secret-", 4)
if err := os.WriteFile(env, []byte("RUSTPROXY_DOCKER_AGENT_TOKEN="+secret+"\n"), 0600); err != nil {
t.Fatal(err)
}
var out, errout bytes.Buffer
if err := run([]string{"--env-file", env, "--help"}, &out, &errout); err != nil {
t.Fatal(err)
}
if strings.Contains(out.String()+errout.String(), secret) {
t.Fatal("--help leaked token")
}
}
func TestCLIRejectsShortTokenWithoutSilentReplacement(t *testing.T) {
dir := t.TempDir()
env := filepath.Join(dir, "agent.env")
if err := os.WriteFile(env, []byte("RUSTPROXY_DOCKER_AGENT_TOKEN=weak\n"), 0600); err != nil {
t.Fatal(err)
}
var out, errout bytes.Buffer
if err := run([]string{"init", "--env-file", env}, &out, &errout); err == nil {
t.Fatal("short custom token must not be silently replaced")
}
got, err := os.ReadFile(env)
if err != nil || !strings.Contains(string(got), "TOKEN=weak") {
t.Fatalf("unexpected file modification: %s %v", got, err)
}
}
func TestCLICertWithoutToken(t *testing.T) {
dir := t.TempDir()
cert := filepath.Join(dir, "cert.crt")
key := filepath.Join(dir, "cert.key")
var out, errout bytes.Buffer
args := []string{"cert-init", "--env-file", filepath.Join(dir, "missing.env"), "--cert-file", cert, "--key-file", key}
if err := run(args, &out, &errout); err != nil {
t.Fatal(err)
}
if len(strings.TrimSpace(out.String())) != 64 {
t.Fatal("invalid SHA fingerprint")
}
}
func TestCLIInfoCanReportMissingCertificate(t *testing.T) {
dir := t.TempDir()
var out, errout bytes.Buffer
err := run([]string{"info", "--env-file", filepath.Join(dir, "missing.env"), "--cert-file", filepath.Join(dir, "no.crt")}, &out, &errout)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(out.String(), "TLS certificate: missing") {
t.Fatalf("info: %s", out.String())
}
}
func TestCLIDoesNotFollowSymlinkToProvisionToken(t *testing.T) {
dir := t.TempDir()
original := filepath.Join(dir, "original")
if err := os.WriteFile(original, []byte("untouched\n"), 0600); err != nil {
t.Fatal(err)
}
link := filepath.Join(dir, "agent.env")
if err := os.Symlink(original, link); err != nil {
t.Fatal(err)
}
if _, _, err := provisionToken(link, true); err == nil {
t.Fatal("symlink must be rejected")
}
got, _ := os.ReadFile(original)
if string(got) != "untouched\n" {
t.Fatal("target file was modified")
}
}
func TestCLIRejectsExamplePlaceholderAsServerToken(t *testing.T) {
if validToken("replace-with-at-least-24-random-characters") {
t.Fatal("example placeholder accepted as a token")
}
if !validToken(strings.Repeat("t", 24)) {
t.Fatal("valid random-like token incorrectly rejected")
}
}