Compare commits

...

1 Commits

Author SHA1 Message Date
core-be 1e7c4e162e test(handlers/workspace_crud): add workspace_crud_test.go — 16 cases for pure validation helpers
Covers:
- validateWorkspaceID: valid/invalid UUIDs
- validateWorkspaceFields: newline rejection, YAML special chars, length limits
- validateWorkspaceDir: absolute paths, relative paths, double-dot traversal, system paths

Branch: feat/workspace-crud-validation-tests

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 08:18:22 +00:00
@@ -0,0 +1,331 @@
package handlers
import (
"strings"
"testing"
)
// ─────────────────────────────────────────────────────────────────────────────
// validateWorkspaceID tests
// ─────────────────────────────────────────────────────────────────────────────
func TestValidateWorkspaceID_Valid(t *testing.T) {
valid := []string{
"550e8400-e29b-41d4-a716-446655440000",
"f47ac10b-58cc-4372-a567-0e02b2c3d479",
"00000000-0000-0000-0000-000000000000",
"123e4567-e89b-12d3-a456-426614174000",
}
for _, id := range valid {
if err := validateWorkspaceID(id); err != nil {
t.Errorf("valid UUID %q: unexpected error: %v", id, err)
}
}
}
func TestValidateWorkspaceID_Invalid(t *testing.T) {
invalid := []string{
"",
"not-a-uuid",
"550e8400-e29b-41d4-a716", // too short
"550e8400-e29b-41d4-a716-44665544000", // too short (missing digit)
"550e8400-e29b-41d4-a716-4466554400000", // too long
"550e8400e29b41d4a716446655440000", // no dashes
"550e8400-e29b-41d4-a716-44665544000g", // invalid hex char
"../../etc/passwd",
"../../../secret",
"-1",
"0",
"workspace-123",
}
for _, id := range invalid {
if err := validateWorkspaceID(id); err == nil {
t.Errorf("invalid ID %q: expected error, got nil", id)
}
}
}
// ─────────────────────────────────────────────────────────────────────────────
// validateWorkspaceFields tests
// ─────────────────────────────────────────────────────────────────────────────
// yamlSpecialChars is the set banned by validateWorkspaceFields.
const yamlSpecialChars = "{}[]|>*&!"
func TestValidateWorkspaceFields_Valid(t *testing.T) {
err := validateWorkspaceFields("my-workspace", "backend", "claude-3-5-sonnet", "docker")
if err != nil {
t.Errorf("valid fields: unexpected error: %v", err)
}
}
func TestValidateWorkspaceFields_Empty(t *testing.T) {
// Empty strings are valid (all-zero-length passes the checks).
err := validateWorkspaceFields("", "", "", "")
if err != nil {
t.Errorf("empty fields: unexpected error: %v", err)
}
}
func TestValidateWorkspaceFields_NameNewline(t *testing.T) {
for _, nl := range []string{"\n", "\r", "\r\n"} {
err := validateWorkspaceFields("bad\nname", "", "", "")
if err == nil {
t.Errorf("name with CR: expected error, got nil")
}
}
}
func TestValidateWorkspaceFields_RoleNewline(t *testing.T) {
err := validateWorkspaceFields("", "bad\rrole", "", "")
if err == nil {
t.Errorf("role with CR: expected error, got nil")
}
}
func TestValidateWorkspaceFields_NameYAMLSpecialChars(t *testing.T) {
for _, ch := range strings.Split(yamlSpecialChars, "") {
name := "name" + ch
err := validateWorkspaceFields(name, "", "", "")
if err == nil {
t.Errorf("name with YAML special char %q: expected error, got nil", ch)
}
}
}
func TestValidateWorkspaceFields_RoleYAMLSpecialChars(t *testing.T) {
for _, ch := range strings.Split(yamlSpecialChars, "") {
role := "role" + ch
err := validateWorkspaceFields("", role, "", "")
if err == nil {
t.Errorf("role with YAML special char %q: expected error, got nil", ch)
}
}
}
// Name: 256 chars should fail; 255 should pass.
func TestValidateWorkspaceFields_NameLength(t *testing.T) {
long := strings.Repeat("a", 256)
err := validateWorkspaceFields(long, "", "", "")
if err == nil {
t.Errorf("name 256 chars: expected error, got nil")
}
err = validateWorkspaceFields(strings.Repeat("a", 255), "", "", "")
if err != nil {
t.Errorf("name 255 chars: unexpected error: %v", err)
}
}
// Role: 1001 chars should fail; 1000 should pass.
func TestValidateWorkspaceFields_RoleLength(t *testing.T) {
long := strings.Repeat("a", 1001)
err := validateWorkspaceFields("", long, "", "")
if err == nil {
t.Errorf("role 1001 chars: expected error, got nil")
}
err = validateWorkspaceFields("", strings.Repeat("a", 1000), "", "")
if err != nil {
t.Errorf("role 1000 chars: unexpected error: %v", err)
}
}
// Model: 101 chars should fail; 100 should pass.
func TestValidateWorkspaceFields_ModelLength(t *testing.T) {
long := strings.Repeat("a", 101)
err := validateWorkspaceFields("", "", long, "")
if err == nil {
t.Errorf("model 101 chars: expected error, got nil")
}
err = validateWorkspaceFields("", "", strings.Repeat("a", 100), "")
if err != nil {
t.Errorf("model 100 chars: unexpected error: %v", err)
}
}
// Runtime: 101 chars should fail; 100 should pass.
func TestValidateWorkspaceFields_RuntimeLength(t *testing.T) {
long := strings.Repeat("a", 101)
err := validateWorkspaceFields("", "", "", long)
if err == nil {
t.Errorf("runtime 101 chars: expected error, got nil")
}
err = validateWorkspaceFields("", "", "", strings.Repeat("a", 100))
if err != nil {
t.Errorf("runtime 100 chars: unexpected error: %v", err)
}
}
// Newlines in model and runtime should also be rejected.
func TestValidateWorkspaceFields_ModelNewline(t *testing.T) {
err := validateWorkspaceFields("", "", "model\nname", "")
if err == nil {
t.Errorf("model with newline: expected error, got nil")
}
}
func TestValidateWorkspaceFields_RuntimeNewline(t *testing.T) {
err := validateWorkspaceFields("", "", "", "docker\n")
if err == nil {
t.Errorf("runtime with newline: expected error, got nil")
}
}
// Valid punctuation does not trigger YAML special char rejection.
func TestValidateWorkspaceFields_NameAllowedPunctuation(t *testing.T) {
valid := []string{
"my-workspace",
"workspace.1",
"workspace_1",
"workspace@prod",
"workspace (1)",
"workspace:backend",
"workspace+prod",
"workspace=1",
"workspace?yes",
"workspace&test",
}
for _, name := range valid {
err := validateWorkspaceFields(name, "", "", "")
if err != nil {
t.Errorf("name %q: unexpected error: %v", name, err)
}
}
}
// ─────────────────────────────────────────────────────────────────────────────
// validateWorkspaceDir tests
// ─────────────────────────────────────────────────────────────────────────────
func TestValidateWorkspaceDir_ValidAbsolute(t *testing.T) {
valid := []string{
"/home/user/workspaces/my-workspace",
"/opt/molecule/workspaces/ws1",
"/var/data/workspaces",
"/Users/test/workspace",
"/workspace/123",
"/a",
}
for _, dir := range valid {
err := validateWorkspaceDir(dir)
if err != nil {
t.Errorf("valid path %q: unexpected error: %v", dir, err)
}
}
}
func TestValidateWorkspaceDir_RelativePath(t *testing.T) {
invalid := []string{
"relative/path",
"./workspace",
"../workspace",
"workspace",
"",
"./../etc/passwd",
}
for _, dir := range invalid {
err := validateWorkspaceDir(dir)
if err == nil {
t.Errorf("relative path %q: expected error, got nil", dir)
}
}
}
func TestValidateWorkspaceDir_DoubleDot(t *testing.T) {
invalid := []string{
"/home/user/../etc/passwd",
"/workspace/../../root",
"/opt/../opt/../etc",
"/home/workspace/..",
"/a/b/../../c",
}
for _, dir := range invalid {
err := validateWorkspaceDir(dir)
if err == nil {
t.Errorf("path with .. %q: expected error, got nil", dir)
}
}
}
func TestValidateWorkspaceDir_SystemPaths(t *testing.T) {
systemPaths := []string{
"/etc",
"/etc/",
"/etc/passwd",
"/var",
"/var/log",
"/proc",
"/proc/self",
"/sys",
"/sys/kernel",
"/dev",
"/dev/null",
"/boot",
"/boot/vmlinuz",
"/sbin",
"/sbin/init",
"/bin",
"/bin/sh",
"/lib",
"/lib64",
"/usr",
"/usr/bin",
"/usr/local",
"/usr/local/bin",
}
for _, dir := range systemPaths {
err := validateWorkspaceDir(dir)
if err == nil {
t.Errorf("system path %q: expected error, got nil", dir)
}
}
}
func TestValidateWorkspaceDir_NonSystemUnderSystem(t *testing.T) {
// Paths that START WITH a system prefix are also rejected.
underSystem := []string{
"/etc/something",
"/var/log/molecule",
"/usr/local/share",
"/usr/share",
}
for _, dir := range underSystem {
err := validateWorkspaceDir(dir)
if err == nil {
t.Errorf("path under system root %q: expected error, got nil", dir)
}
}
}
func TestValidateWorkspaceDir_NotUnderSystem(t *testing.T) {
// Paths that merely contain a system prefix as a segment, but don't start with it, are fine.
valid := []string{
"/home/etc/something",
"/opt/var/workspace",
"/molecule/etc/passwd",
"/varuser/local",
"/usrenv/bin",
}
for _, dir := range valid {
err := validateWorkspaceDir(dir)
if err != nil {
t.Errorf("path %q: unexpected error: %v", dir, err)
}
}
}
// Paths that resolve (after Clean) to a system path are also rejected.
func TestValidateWorkspaceDir_PathTraversalResolvesToSystem(t *testing.T) {
// These are all technically relative paths that would be rejected as non-absolute,
// but the ".." cases above cover the traversal semantics. Here we verify that
// even paths that look absolute but have ".." segments are caught.
invalid := []string{
"/home/user/../../etc",
"/a/b/../../../etc",
}
for _, dir := range invalid {
err := validateWorkspaceDir(dir)
if err == nil {
t.Errorf("path %q: expected error, got nil", dir)
}
}
}