Add tests for main package, manager, and various components

- Introduced unit tests for the main package to ensure compilation.
- Added tests for the manager, including validation of upload sessions and handling of Blender binary paths.
- Implemented tests for job token generation and validation, ensuring security and integrity.
- Created tests for configuration management and database schema to verify functionality.
- Added tests for logger and runner components to enhance overall test coverage and reliability.
This commit is contained in:
2026-03-14 22:20:03 -05:00
parent 16d6a95058
commit a3defe5cf6
45 changed files with 1717 additions and 52 deletions

View File

@@ -97,6 +97,58 @@ func (s *Manager) failUploadSession(sessionID, errorMessage string) (int64, bool
return userID, true
}
const (
uploadSessionExpiredCode = "UPLOAD_SESSION_EXPIRED"
uploadSessionNotReadyCode = "UPLOAD_SESSION_NOT_READY"
)
type uploadSessionValidationError struct {
Code string
Message string
}
func (e *uploadSessionValidationError) Error() string {
return e.Message
}
// validateUploadSessionForJobCreation validates that an upload session can be used for job creation.
// Returns the session and its context tar path when valid.
func (s *Manager) validateUploadSessionForJobCreation(sessionID string, userID int64) (*UploadSession, string, error) {
s.uploadSessionsMu.RLock()
uploadSession := s.uploadSessions[sessionID]
s.uploadSessionsMu.RUnlock()
if uploadSession == nil || uploadSession.UserID != userID {
return nil, "", &uploadSessionValidationError{
Code: uploadSessionExpiredCode,
Message: "Upload session expired or not found. Please upload the file again.",
}
}
if uploadSession.Status != "completed" {
return nil, "", &uploadSessionValidationError{
Code: uploadSessionNotReadyCode,
Message: "Upload session is not ready yet. Wait for processing to complete.",
}
}
if uploadSession.TempDir == "" {
return nil, "", &uploadSessionValidationError{
Code: uploadSessionExpiredCode,
Message: "Upload session context data is missing. Please upload the file again.",
}
}
tempContextPath := filepath.Join(uploadSession.TempDir, "context.tar")
if _, statErr := os.Stat(tempContextPath); statErr != nil {
log.Printf("ERROR: Context archive not found at %s for session %s: %v", tempContextPath, sessionID, statErr)
return nil, "", &uploadSessionValidationError{
Code: uploadSessionExpiredCode,
Message: "Upload session context archive was not found (possibly after manager restart). Please upload the file again.",
}
}
return uploadSession, tempContextPath, nil
}
// handleCreateJob creates a new job
func (s *Manager) handleCreateJob(w http.ResponseWriter, r *http.Request) {
userID, err := getUserID(r)
@@ -178,6 +230,22 @@ func (s *Manager) handleCreateJob(w http.ResponseWriter, r *http.Request) {
}
}
var uploadSession *UploadSession
var tempContextPath string
if req.UploadSessionID != nil && *req.UploadSessionID != "" {
var validateErr error
uploadSession, tempContextPath, validateErr = s.validateUploadSessionForJobCreation(*req.UploadSessionID, userID)
if validateErr != nil {
var sessionErr *uploadSessionValidationError
if errors.As(validateErr, &sessionErr) {
s.respondErrorWithCode(w, http.StatusBadRequest, sessionErr.Code, sessionErr.Message)
} else {
s.respondError(w, http.StatusBadRequest, validateErr.Error())
}
return
}
}
// Store render settings, unhide_objects, enable_execution, and blender_version in blend_metadata if provided.
var blendMetadataJSON *string
if req.RenderSettings != nil || req.UnhideObjects != nil || req.EnableExecution != nil || req.BlenderVersion != nil || req.OutputFormat != nil {
@@ -226,39 +294,29 @@ func (s *Manager) handleCreateJob(w http.ResponseWriter, r *http.Request) {
s.respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to create job: %v", err))
return
}
cleanupCreatedJob := func(reason string) {
log.Printf("Cleaning up partially created job %d: %s", jobID, reason)
_ = s.db.With(func(conn *sql.DB) error {
// Be defensive in case foreign key cascade is disabled.
_, _ = conn.Exec(`DELETE FROM task_logs WHERE task_id IN (SELECT id FROM tasks WHERE job_id = ?)`, jobID)
_, _ = conn.Exec(`DELETE FROM task_steps WHERE task_id IN (SELECT id FROM tasks WHERE job_id = ?)`, jobID)
_, _ = conn.Exec(`DELETE FROM tasks WHERE job_id = ?`, jobID)
_, _ = conn.Exec(`DELETE FROM job_files WHERE job_id = ?`, jobID)
_, _ = conn.Exec(`DELETE FROM jobs WHERE id = ?`, jobID)
return nil
})
_ = os.RemoveAll(s.storage.JobPath(jobID))
}
// If upload session ID is provided, move the context archive from temp to job directory
if req.UploadSessionID != nil && *req.UploadSessionID != "" {
if uploadSession != nil {
log.Printf("Processing upload session for job %d: %s", jobID, *req.UploadSessionID)
var uploadSession *UploadSession
s.uploadSessionsMu.RLock()
uploadSession = s.uploadSessions[*req.UploadSessionID]
s.uploadSessionsMu.RUnlock()
if uploadSession == nil || uploadSession.UserID != userID {
s.respondError(w, http.StatusBadRequest, "Invalid upload session. Please upload the file again.")
return
}
if uploadSession.Status != "completed" {
s.respondError(w, http.StatusBadRequest, "Upload session is not ready yet. Wait for processing to complete.")
return
}
if uploadSession.TempDir == "" {
s.respondError(w, http.StatusBadRequest, "Upload session is missing context data. Please upload again.")
return
}
tempContextPath := filepath.Join(uploadSession.TempDir, "context.tar")
if _, statErr := os.Stat(tempContextPath); statErr != nil {
log.Printf("ERROR: Context archive not found at %s for session %s: %v", tempContextPath, *req.UploadSessionID, statErr)
s.respondError(w, http.StatusBadRequest, "Context archive not found for upload session. Please upload the file again.")
return
}
log.Printf("Found context archive at %s, moving to job %d directory", tempContextPath, jobID)
jobPath := s.storage.JobPath(jobID)
if err := os.MkdirAll(jobPath, 0755); err != nil {
log.Printf("ERROR: Failed to create job directory for job %d: %v", jobID, err)
cleanupCreatedJob("failed to create job directory")
s.respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to create job directory: %v", err))
return
}
@@ -267,6 +325,7 @@ func (s *Manager) handleCreateJob(w http.ResponseWriter, r *http.Request) {
srcFile, err := os.Open(tempContextPath)
if err != nil {
log.Printf("ERROR: Failed to open source context archive %s: %v", tempContextPath, err)
cleanupCreatedJob("failed to open source context archive")
s.respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to open context archive: %v", err))
return
}
@@ -275,6 +334,7 @@ func (s *Manager) handleCreateJob(w http.ResponseWriter, r *http.Request) {
dstFile, err := os.Create(jobContextPath)
if err != nil {
log.Printf("ERROR: Failed to create destination context archive %s: %v", jobContextPath, err)
cleanupCreatedJob("failed to create destination context archive")
s.respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to create context archive: %v", err))
return
}
@@ -284,6 +344,7 @@ func (s *Manager) handleCreateJob(w http.ResponseWriter, r *http.Request) {
dstFile.Close()
os.Remove(jobContextPath)
log.Printf("ERROR: Failed to copy context archive from %s to %s: %v", tempContextPath, jobContextPath, err)
cleanupCreatedJob("failed to copy context archive")
s.respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to copy context archive: %v", err))
return
}
@@ -291,6 +352,7 @@ func (s *Manager) handleCreateJob(w http.ResponseWriter, r *http.Request) {
srcFile.Close()
if err := dstFile.Close(); err != nil {
log.Printf("ERROR: Failed to close destination file: %v", err)
cleanupCreatedJob("failed to finalize destination context archive")
s.respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to finalize context archive: %v", err))
return
}
@@ -301,6 +363,7 @@ func (s *Manager) handleCreateJob(w http.ResponseWriter, r *http.Request) {
contextInfo, err := os.Stat(jobContextPath)
if err != nil {
log.Printf("ERROR: Failed to stat context archive after move: %v", err)
cleanupCreatedJob("failed to stat copied context archive")
s.respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to verify context archive: %v", err))
return
}
@@ -320,6 +383,7 @@ func (s *Manager) handleCreateJob(w http.ResponseWriter, r *http.Request) {
})
if err != nil {
log.Printf("ERROR: Failed to record context archive in database for job %d: %v", jobID, err)
cleanupCreatedJob("failed to record context archive in database")
s.respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to record context archive: %v", err))
return
}
@@ -382,6 +446,7 @@ func (s *Manager) handleCreateJob(w http.ResponseWriter, r *http.Request) {
return nil
})
if err != nil {
cleanupCreatedJob("failed to create render tasks")
s.respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to create tasks: %v", err))
return
}
@@ -1984,10 +2049,14 @@ func (s *Manager) runBlenderMetadataExtraction(blendFile, workDir, blenderVersio
return nil, fmt.Errorf("failed to create extraction script: %w", err)
}
// Make blend file path relative to workDir to avoid path resolution issues
blendFileRel, err := filepath.Rel(workDir, blendFile)
// Use absolute paths to avoid path normalization issues with relative traversal.
blendFileAbs, err := filepath.Abs(blendFile)
if err != nil {
return nil, fmt.Errorf("failed to get relative path for blend file: %w", err)
return nil, fmt.Errorf("failed to get absolute path for blend file: %w", err)
}
scriptPathAbs, err := filepath.Abs(scriptPath)
if err != nil {
return nil, fmt.Errorf("failed to get absolute path for extraction script: %w", err)
}
// Determine which blender binary to use
@@ -2037,11 +2106,17 @@ func (s *Manager) runBlenderMetadataExtraction(blendFile, workDir, blenderVersio
}
}
// Ensure Blender binary is always an absolute path.
blenderBinary, err = resolveBlenderBinaryPath(blenderBinary)
if err != nil {
return nil, err
}
// Execute Blender using executils (set LD_LIBRARY_PATH for tarball installs)
runEnv := blender.TarballEnv(blenderBinary, os.Environ())
result, err := executils.RunCommand(
blenderBinary,
[]string{"-b", blendFileRel, "--python", "extract_metadata.py"},
[]string{"-b", blendFileAbs, "--python", scriptPathAbs},
workDir,
runEnv,
0, // no task ID for metadata extraction