Files
yawan f7fc992f57
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
2026-09-22 16:19:06 +08:00

114 lines
3.5 KiB
Go

package cmdutil
import (
"errors"
"strings"
"testing"
)
// TestValidateProfileName_AcceptsAllowlist verifies the documented charset
// is accepted as-is.
func TestValidateProfileName_AcceptsAllowlist(t *testing.T) {
for _, name := range []string{
"default",
"prod",
"staging-2026",
"ci_runner",
"alice.example",
"a", // single char
"A-Z_0-9",
} {
if err := ValidateProfileName(name); err != nil {
t.Errorf("ValidateProfileName(%q) unexpected error: %v", name, err)
}
}
}
// TestValidateProfileName_RejectsEmpty guards the empty-string base case.
func TestValidateProfileName_RejectsEmpty(t *testing.T) {
err := ValidateProfileName("")
if err == nil {
t.Fatal("expected error for empty name")
}
var ce *Error
if !errors.As(err, &ce) || ce.Code != CodeInputInvalidArgument {
t.Errorf("expected input.invalid_argument, got %v", err)
}
}
// TestValidateProfileName_RejectsShellMetachars is the security-critical
// case: anything that could break retry_argv shell interpolation must be
// rejected at the entry point. If this test ever loosens, an agent that
// joins-and-exec()s retry_argv becomes injectable via a malicious profile name.
func TestValidateProfileName_RejectsShellMetachars(t *testing.T) {
cases := []string{
"evil; rm -rf /",
"foo && bar",
"foo || bar",
"foo|bar",
"foo`whoami`",
"foo$(whoami)",
"foo$bar",
"foo>out",
"foo<in",
"foo bar", // space alone
"foo'bar",
`foo"bar`,
"foo\nbar", // newline
"foo\tbar", // tab
"foo\\bar", // backslash (also path-like)
"foo/bar", // slash (also path-like)
"#foo", // comment marker
"foo*bar", // glob
"foo?bar", // glob
"foo~bar", // home expansion
"foo!bar", // history expansion
"foo,bar", // brace-expansion-ish
"foo[bar",
}
for _, name := range cases {
err := ValidateProfileName(name)
if err == nil {
t.Errorf("ValidateProfileName(%q) should have rejected the name; a name echoed into retry_argv / prose would be injectable", name)
continue
}
var ce *Error
if !errors.As(err, &ce) || ce.Code != CodeInputInvalidArgument {
t.Errorf("ValidateProfileName(%q) returned wrong code %v; want input.invalid_argument", name, err)
}
}
}
// TestValidateProfileName_RejectsPathTraversal covers the keyring-namespace
// escape vector. `.` and `..` are reserved, and any slash is rejected.
func TestValidateProfileName_RejectsPathTraversal(t *testing.T) {
for _, name := range []string{".", "..", "../foo", "foo/..", "a/b", `a\b`} {
err := ValidateProfileName(name)
if err == nil {
t.Errorf("ValidateProfileName(%q) should have rejected the path-like name", name)
continue
}
// Path-shaped names hit the dedicated "reserved or path-like" branch
// first (clearer hint than "invalid character %q") for `..` and the
// slashed forms.
if name == "." || name == ".." || strings.ContainsAny(name, "/\\") {
if !strings.Contains(err.Error(), "reserved or path-like") {
t.Errorf("ValidateProfileName(%q): expected path-like hint, got %v", name, err)
}
}
}
}
// TestValidateProfileName_HintMentionsAllowlist verifies the user-facing
// hint actually tells them what's allowed (not just "invalid").
func TestValidateProfileName_HintMentionsAllowlist(t *testing.T) {
err := ValidateProfileName("foo bar")
var ce *Error
if !errors.As(err, &ce) {
t.Fatalf("expected typed Error, got %v", err)
}
if !strings.Contains(ce.Hint, "letters") || !strings.Contains(ce.Hint, "dots") {
t.Errorf("hint should describe allowlist; got %q", ce.Hint)
}
}