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") } }