- Fix YAML tags in auth config struct (json -> yaml) - Update CLI configs to use pre-hashed API keys - Remove double hashing in WebSocket client - Fix port mapping (9102 -> 9103) in CLI commands - Update permission keys to use jobs:read, jobs:create, etc. - Clean up all debug logging from CLI and server - All user roles now authenticate correctly: * Admin: Can queue jobs and see all jobs * Researcher: Can queue jobs and see own jobs * Analyst: Can see status (read-only access) Multi-user authentication is now fully functional.
238 lines
7.3 KiB
Go
238 lines
7.3 KiB
Go
package tests
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/jfraeys/fetch_ml/internal/fileutil"
|
|
)
|
|
|
|
// TestZeroInstallWorkflow tests the complete minimal zero-install workflow
|
|
func TestZeroInstallWorkflow(t *testing.T) {
|
|
t.Parallel() // Enable parallel execution
|
|
|
|
// Setup test environment
|
|
testDir := t.TempDir()
|
|
|
|
// Step 1: Create experiment locally (simulating DS workflow)
|
|
experimentDir := filepath.Join(testDir, "my_experiment")
|
|
if err := os.MkdirAll(experimentDir, 0750); err != nil {
|
|
t.Fatalf("Failed to create experiment directory: %v", err)
|
|
}
|
|
|
|
// Create train.py (simplified from README example)
|
|
trainScript := filepath.Join(experimentDir, "train.py")
|
|
trainCode := `import argparse, json, logging, time
|
|
from pathlib import Path
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--epochs", type=int, default=10)
|
|
parser.add_argument("--output_dir", type=str, required=True)
|
|
args = parser.parse_args()
|
|
|
|
logging.basicConfig(level=logging.INFO)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
logger.info(f"Training for {args.epochs} epochs...")
|
|
|
|
for epoch in range(args.epochs):
|
|
loss = 1.0 - (epoch * 0.1)
|
|
accuracy = 0.5 + (epoch * 0.045)
|
|
logger.info(f"Epoch {epoch + 1}: loss={loss:.4f}, acc={accuracy:.4f}")
|
|
time.sleep(0.1) // Reduced from 0.5
|
|
|
|
results = {"accuracy": accuracy, "loss": loss, "epochs": args.epochs}
|
|
|
|
output_dir = Path(args.output_dir)
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
with open(output_dir / "results.json", "w") as f:
|
|
json.dump(results, f)
|
|
|
|
logger.info("Training complete!")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
`
|
|
|
|
//nolint:gosec // G306: Script needs execute permissions
|
|
if err := os.WriteFile(trainScript, []byte(trainCode), 0750); err != nil {
|
|
t.Fatalf("Failed to create train.py: %v", err)
|
|
}
|
|
|
|
// Test 1: Verify experiment structure (Step 1 validation)
|
|
t.Run("Step1_CreateExperiment", func(t *testing.T) {
|
|
// Check train.py exists and is executable
|
|
if _, err := os.Stat(trainScript); os.IsNotExist(err) {
|
|
t.Error("train.py should exist after experiment creation")
|
|
}
|
|
|
|
info, err := os.Stat(trainScript)
|
|
if err != nil {
|
|
t.Fatalf("Failed to stat train.py: %v", err)
|
|
}
|
|
if info.Mode().Perm()&0111 == 0 {
|
|
t.Error("train.py should be executable")
|
|
}
|
|
})
|
|
|
|
// Step 2: Simulate upload process (rsync simulation)
|
|
t.Run("Step2_UploadExperiment", func(t *testing.T) {
|
|
// Create server directory structure (simulate ml-server.company.com)
|
|
serverDir := filepath.Join(testDir, "server")
|
|
homeDir := filepath.Join(serverDir, "home", "mluser")
|
|
pendingDir := filepath.Join(homeDir, "ml_jobs", "pending")
|
|
|
|
// Generate timestamp-based job name (simulating workflow)
|
|
jobName := "my_experiment_20231201_143022"
|
|
jobDir := filepath.Join(pendingDir, jobName)
|
|
|
|
if err := os.MkdirAll(jobDir, 0750); err != nil {
|
|
t.Fatalf("Failed to create server directories: %v", err)
|
|
}
|
|
|
|
// Simulate rsync upload (copy experiment files)
|
|
files := []string{"train.py"}
|
|
for _, file := range files {
|
|
src := filepath.Join(experimentDir, file)
|
|
dst := filepath.Join(jobDir, file)
|
|
|
|
data, err := fileutil.SecureFileRead(src)
|
|
if err != nil {
|
|
t.Fatalf("Failed to read %s: %v", file, err)
|
|
}
|
|
|
|
//nolint:gosec // G306: Script needs execute permissions
|
|
if err := os.WriteFile(dst, data, 0750); err != nil {
|
|
t.Fatalf("Failed to copy %s: %v", file, err)
|
|
}
|
|
}
|
|
|
|
// Verify upload succeeded
|
|
for _, file := range files {
|
|
dst := filepath.Join(jobDir, file)
|
|
if _, err := os.Stat(dst); os.IsNotExist(err) {
|
|
t.Errorf("Uploaded file %s should exist in pending directory", file)
|
|
}
|
|
}
|
|
|
|
// Verify job directory structure
|
|
if _, err := os.Stat(pendingDir); os.IsNotExist(err) {
|
|
t.Error("Pending directory should exist")
|
|
}
|
|
if _, err := os.Stat(jobDir); os.IsNotExist(err) {
|
|
t.Error("Job directory should exist")
|
|
}
|
|
})
|
|
|
|
// Step 3: Simulate TUI access (minimal - just verify TUI would launch)
|
|
t.Run("Step3_TUIAccess", func(t *testing.T) {
|
|
// Create fetch_ml directory structure (simulating server setup)
|
|
serverDir := filepath.Join(testDir, "server")
|
|
fetchMlDir := filepath.Join(serverDir, "home", "mluser", "fetch_ml")
|
|
buildDir := filepath.Join(fetchMlDir, "build")
|
|
configsDir := filepath.Join(fetchMlDir, "configs")
|
|
|
|
if err := os.MkdirAll(buildDir, 0750); err != nil {
|
|
t.Fatalf("Failed to create fetch_ml directories: %v", err)
|
|
}
|
|
if err := os.MkdirAll(configsDir, 0750); err != nil {
|
|
t.Fatalf("Failed to create configs directory: %v", err)
|
|
}
|
|
|
|
// Create mock TUI binary
|
|
tuiBinary := filepath.Join(buildDir, "tui")
|
|
tuiContent := "#!/bin/bash\necho 'Mock TUI would launch here'"
|
|
//nolint:gosec // G306: Script needs execute permissions
|
|
if err := os.WriteFile(tuiBinary, []byte(tuiContent), 0750); err != nil {
|
|
t.Fatalf("Failed to create mock TUI binary: %v", err)
|
|
}
|
|
|
|
// Create config file
|
|
configFile := filepath.Join(configsDir, "config.yaml")
|
|
configContent := `server:
|
|
host: "localhost"
|
|
port: 8080
|
|
|
|
redis:
|
|
addr: "localhost:6379"
|
|
db: 0
|
|
|
|
data_dir: "/home/mluser/datasets"
|
|
output_dir: "/home/mluser/ml_jobs"
|
|
`
|
|
if err := os.WriteFile(configFile, []byte(configContent), 0600); err != nil {
|
|
t.Fatalf("Failed to create config file: %v", err)
|
|
}
|
|
|
|
// Verify TUI setup
|
|
if _, err := os.Stat(tuiBinary); os.IsNotExist(err) {
|
|
t.Error("TUI binary should exist")
|
|
}
|
|
if _, err := os.Stat(configFile); os.IsNotExist(err) {
|
|
t.Error("Config file should exist")
|
|
}
|
|
})
|
|
|
|
// Test: Verify complete workflow files exist
|
|
t.Run("CompleteWorkflowValidation", func(t *testing.T) {
|
|
// Verify experiment files exist
|
|
if _, err := os.Stat(trainScript); os.IsNotExist(err) {
|
|
t.Error("Experiment train.py should exist")
|
|
}
|
|
|
|
// Verify uploaded files exist
|
|
uploadedTrainScript := filepath.Join(
|
|
testDir, "server", "home", "mluser", "ml_jobs", "pending",
|
|
"my_experiment_20231201_143022", "train.py")
|
|
if _, err := os.Stat(uploadedTrainScript); os.IsNotExist(err) {
|
|
t.Error("Uploaded train.py should exist in pending directory")
|
|
}
|
|
|
|
// Verify TUI setup exists
|
|
tuiBinary := filepath.Join(testDir, "server", "home", "mluser", "fetch_ml", "build", "tui")
|
|
if _, err := os.Stat(tuiBinary); os.IsNotExist(err) {
|
|
t.Error("TUI binary should exist for workflow completion")
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestMinimalWorkflowSecurity tests security aspects of minimal workflow
|
|
func TestMinimalWorkflowSecurity(t *testing.T) {
|
|
t.Parallel() // Enable parallel execution
|
|
testDir := t.TempDir()
|
|
|
|
// Create mock SSH environment
|
|
sshRc := filepath.Join(testDir, "sshrc")
|
|
sshRcContent := `#!/bin/bash
|
|
# Mock SSH rc - TUI only
|
|
if [ -n "$SSH_CONNECTION" ] && [ -z "$SSH_ORIGINAL_COMMAND" ]; then
|
|
echo "TUI would launch here"
|
|
else
|
|
echo "Command execution blocked for security"
|
|
exit 1
|
|
fi
|
|
`
|
|
|
|
//nolint:gosec // G306: Script needs execute permissions
|
|
if err := os.WriteFile(sshRc, []byte(sshRcContent), 0750); err != nil {
|
|
t.Fatalf("Failed to create SSH rc: %v", err)
|
|
}
|
|
|
|
t.Run("TUIOnlyAccess", func(t *testing.T) {
|
|
// Verify SSH rc exists and is executable
|
|
if _, err := os.Stat(sshRc); os.IsNotExist(err) {
|
|
t.Error("SSH rc should exist")
|
|
}
|
|
|
|
info, err := os.Stat(sshRc)
|
|
if err != nil {
|
|
t.Fatalf("Failed to stat SSH rc: %v", err)
|
|
}
|
|
if info.Mode().Perm()&0111 == 0 {
|
|
t.Error("SSH rc should be executable")
|
|
}
|
|
})
|
|
}
|