test(handlers): add workspace_crud validation helper tests (#713)
Covers the three pure validator functions introduced in #685/#688:
validateWorkspaceID(id):
- valid UUID forms (nil error)
- empty, traversal, SQL injection, short, invalid hex → error
validateWorkspaceDir(dir):
- absolute non-system paths → nil
- relative paths → error
- traversal sequences (..) → error
- system paths (/etc, /proc, /sys, /dev, /boot, /sbin, /bin,
/lib, /usr, /var) → error
- prefixes of system paths → error
validateWorkspaceFields(name, role, model, runtime):
- all-empty → nil
- valid values → nil
- name > 255 chars → error; exactly 255 → nil
- role > 1000 chars → error
- model > 100 chars → error
- runtime > 100 chars → error
- \n or \r in any field → error
- YAML special chars ({ } [ ] | > * & !) in name/role → error
- YAML chars allowed in model/runtime (only name/role are gated)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
d96e6f68d3
commit
dccc8f53cb
@ -0,0 +1,263 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// ── validateWorkspaceID ─────────────────────────────────────────────────────────
|
||||
|
||||
func TestValidateWorkspaceID_Valid(t *testing.T) {
|
||||
cases := []string{
|
||||
"550e8400-e29b-41d4-a716-446655440000",
|
||||
"00000000-0000-0000-0000-000000000000",
|
||||
"ffffffff-ffff-ffff-ffff-ffffffffffff",
|
||||
}
|
||||
for _, id := range cases {
|
||||
t.Run(id, func(t *testing.T) {
|
||||
if err := validateWorkspaceID(id); err != nil {
|
||||
t.Errorf("validateWorkspaceID(%q) returned error: %v", id, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateWorkspaceID_Invalid(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
id string
|
||||
}{
|
||||
{"empty", ""},
|
||||
{"not a UUID", "not-a-uuid"},
|
||||
{"traversal attack", "../../etc/passwd"},
|
||||
{"SQL injection", "'; DROP TABLE workspaces;--"},
|
||||
{"UUID too short", "550e8400-e29b-41d4-a716"},
|
||||
{"UUID with invalid hex chars", "550e8400-e29b-41d4-a716-44665544000g"},
|
||||
{"UUID all zeros", "00000000000000000000000000000000"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if err := validateWorkspaceID(tc.id); err == nil {
|
||||
t.Errorf("validateWorkspaceID(%q): expected error, got nil", tc.id)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ── validateWorkspaceDir ───────────────────────────────────────────────────────
|
||||
|
||||
func TestValidateWorkspaceDir_Valid(t *testing.T) {
|
||||
cases := []string{
|
||||
"/opt/molecule/workspaces/dev",
|
||||
"/home/user/.molecule/workspaces",
|
||||
"/var/data/workspace-abc-123",
|
||||
"/opt/services/molecule/tenant-workspaces",
|
||||
}
|
||||
for _, dir := range cases {
|
||||
t.Run(dir, func(t *testing.T) {
|
||||
if err := validateWorkspaceDir(dir); err != nil {
|
||||
t.Errorf("validateWorkspaceDir(%q) returned error: %v", dir, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateWorkspaceDir_RelativeRejected(t *testing.T) {
|
||||
cases := []string{
|
||||
"relative/path",
|
||||
"./myworkspace",
|
||||
"~/workspaces/dev",
|
||||
}
|
||||
for _, dir := range cases {
|
||||
t.Run(dir, func(t *testing.T) {
|
||||
if err := validateWorkspaceDir(dir); err == nil {
|
||||
t.Errorf("validateWorkspaceDir(%q): expected error (relative path), got nil", dir)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateWorkspaceDir_TraversalRejected(t *testing.T) {
|
||||
cases := []string{
|
||||
"/opt/molecule/../../../etc",
|
||||
"/workspaces/dev/../../root",
|
||||
"/opt/../opt/../etc",
|
||||
}
|
||||
for _, dir := range cases {
|
||||
t.Run(dir, func(t *testing.T) {
|
||||
if err := validateWorkspaceDir(dir); err == nil {
|
||||
t.Errorf("validateWorkspaceDir(%q): expected error (traversal), got nil", dir)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateWorkspaceDir_SystemPathsRejected(t *testing.T) {
|
||||
cases := []string{
|
||||
"/etc",
|
||||
"/etc/molecule",
|
||||
"/var",
|
||||
"/var/log",
|
||||
"/proc",
|
||||
"/proc/self",
|
||||
"/sys",
|
||||
"/sys/kernel",
|
||||
"/dev",
|
||||
"/dev/null",
|
||||
"/boot",
|
||||
"/sbin",
|
||||
"/bin",
|
||||
"/lib",
|
||||
"/usr",
|
||||
"/usr/local",
|
||||
}
|
||||
for _, dir := range cases {
|
||||
t.Run(dir, func(t *testing.T) {
|
||||
if err := validateWorkspaceDir(dir); err == nil {
|
||||
t.Errorf("validateWorkspaceDir(%q): expected error (system path), got nil", dir)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateWorkspaceDir_PrefixMatchesBlocked(t *testing.T) {
|
||||
// The blocklist checks prefix so /etc/foo must also be rejected.
|
||||
cases := []string{
|
||||
"/etc/molecule-config",
|
||||
"/var/log/workspace",
|
||||
"/usr/local/bin",
|
||||
"/usr/bin/molecule",
|
||||
}
|
||||
for _, dir := range cases {
|
||||
t.Run(dir, func(t *testing.T) {
|
||||
if err := validateWorkspaceDir(dir); err == nil {
|
||||
t.Errorf("validateWorkspaceDir(%q): expected error (prefix of blocked path), got nil", dir)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ── validateWorkspaceFields ────────────────────────────────────────────────────
|
||||
|
||||
func TestValidateWorkspaceFields_AllEmpty(t *testing.T) {
|
||||
// All empty → valid (creation uses defaults; empty is allowed)
|
||||
if err := validateWorkspaceFields("", "", "", ""); err != nil {
|
||||
t.Errorf("validateWorkspaceFields with all empty: expected nil, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateWorkspaceFields_Valid(t *testing.T) {
|
||||
if err := validateWorkspaceFields("My Workspace", "Backend Engineer", "gpt-4o", "langgraph"); err != nil {
|
||||
t.Errorf("validateWorkspaceFields with valid args: expected nil, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateWorkspaceFields_NameTooLong(t *testing.T) {
|
||||
longName := make([]byte, 256)
|
||||
for i := range longName {
|
||||
longName[i] = 'a'
|
||||
}
|
||||
if err := validateWorkspaceFields(string(longName), "", "", ""); err == nil {
|
||||
t.Error("name > 255 chars: expected error, got nil")
|
||||
}
|
||||
|
||||
// Exactly 255 chars is OK
|
||||
validName := make([]byte, 255)
|
||||
for i := range validName {
|
||||
validName[i] = 'a'
|
||||
}
|
||||
if err := validateWorkspaceFields(string(validName), "", "", ""); err != nil {
|
||||
t.Errorf("name exactly 255 chars: expected nil, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateWorkspaceFields_RoleTooLong(t *testing.T) {
|
||||
longRole := make([]byte, 1001)
|
||||
for i := range longRole {
|
||||
longRole[i] = 'x'
|
||||
}
|
||||
if err := validateWorkspaceFields("", string(longRole), "", ""); err == nil {
|
||||
t.Error("role > 1000 chars: expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateWorkspaceFields_ModelTooLong(t *testing.T) {
|
||||
longModel := make([]byte, 101)
|
||||
for i := range longModel {
|
||||
longModel[i] = 'x'
|
||||
}
|
||||
if err := validateWorkspaceFields("", "", string(longModel), ""); err == nil {
|
||||
t.Error("model > 100 chars: expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateWorkspaceFields_RuntimeTooLong(t *testing.T) {
|
||||
longRuntime := make([]byte, 101)
|
||||
for i := range longRuntime {
|
||||
longRuntime[i] = 'x'
|
||||
}
|
||||
if err := validateWorkspaceFields("", "", "", string(longRuntime)); err == nil {
|
||||
t.Error("runtime > 100 chars: expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateWorkspaceFields_NewlineInName(t *testing.T) {
|
||||
if err := validateWorkspaceFields("My\nWorkspace", "", "", ""); err == nil {
|
||||
t.Error("name with \\n: expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateWorkspaceFields_CRLFInRole(t *testing.T) {
|
||||
if err := validateWorkspaceFields("", "Backend\r\nEngineer", "", ""); err == nil {
|
||||
t.Error("role with \\r\\n: expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateWorkspaceFields_NewlineInModel(t *testing.T) {
|
||||
if err := validateWorkspaceFields("", "", "gpt-\n4o", ""); err == nil {
|
||||
t.Error("model with \\n: expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateWorkspaceFields_NewlineInRuntime(t *testing.T) {
|
||||
if err := validateWorkspaceFields("", "", "", "lang\rgraph"); err == nil {
|
||||
t.Error("runtime with \\r: expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateWorkspaceFields_YAMLSpecialChars(t *testing.T) {
|
||||
// yamlSpecialChars = "{}[]|>*&!"
|
||||
// These must be rejected in name and role.
|
||||
dangerous := []string{
|
||||
"Workspace{evil}",
|
||||
"Workspace[evil]",
|
||||
"Workspace]evil[",
|
||||
"Workspace|evil",
|
||||
"Workspace>evil",
|
||||
"Workspace*evil",
|
||||
"Workspace&evil",
|
||||
"Workspace!evil",
|
||||
"Name{}",
|
||||
"Role[]",
|
||||
}
|
||||
for _, v := range dangerous {
|
||||
t.Run(v, func(t *testing.T) {
|
||||
if err := validateWorkspaceFields(v, "", "", ""); err == nil {
|
||||
t.Errorf("name %q: expected error (YAML special char), got nil", v)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateWorkspaceFields_YAMLCharsAllowedInModelRuntime(t *testing.T) {
|
||||
// YAML special chars are only blocked in name/role, not model/runtime.
|
||||
if err := validateWorkspaceFields("", "", "model{}[]", "runtime*&!"); err != nil {
|
||||
t.Errorf("model/runtime with YAML chars: expected nil, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateWorkspaceFields_YAMLCharsAllowedInEmptyName(t *testing.T) {
|
||||
// Empty name is fine; YAML char restriction is only on non-empty values.
|
||||
if err := validateWorkspaceFields("", "Backend Engineer", "", ""); err != nil {
|
||||
t.Errorf("empty name with valid role: expected nil, got %v", err)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user