1a69fcfd04
- Updated the installation script to clarify that production wrappers do not include fixed test secrets, improving user guidance for local testing. - Modified the jiggablend-manager and jiggablend-runner scripts to remove fixed test configurations, emphasizing the use of local test credentials via `make init-test`. - Enhanced error handling in the runner script to ensure that an API key is provided, improving security and user feedback. - Added new flags and options for the runner, including sandboxing capabilities and GPU ray tracing control, enhancing flexibility for users. - Improved README documentation to reflect changes in usage and configuration, ensuring users have clear instructions for setup and execution.
306 lines
9.2 KiB
Go
306 lines
9.2 KiB
Go
package api
|
|
|
|
import (
|
|
"archive/tar"
|
|
"archive/zip"
|
|
"bytes"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"jiggablend/internal/storage"
|
|
)
|
|
|
|
func TestGenerateAndCheckETag(t *testing.T) {
|
|
etag := generateETag(map[string]interface{}{"a": 1})
|
|
if etag == "" {
|
|
t.Fatal("expected non-empty etag")
|
|
}
|
|
req := httptest.NewRequest("GET", "/x", nil)
|
|
req.Header.Set("If-None-Match", etag)
|
|
if !checkETag(req, etag) {
|
|
t.Fatal("expected etag match")
|
|
}
|
|
}
|
|
|
|
func TestUploadSessionPhase(t *testing.T) {
|
|
if got := uploadSessionPhase("uploading"); got != "upload" {
|
|
t.Fatalf("unexpected phase: %q", got)
|
|
}
|
|
if got := uploadSessionPhase("select_blend"); got != "action_required" {
|
|
t.Fatalf("unexpected phase: %q", got)
|
|
}
|
|
if got := uploadSessionPhase("something_else"); got != "processing" {
|
|
t.Fatalf("unexpected fallback phase: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestParseTarHeader_AndTruncateString(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
tw := tar.NewWriter(&buf)
|
|
_ = tw.WriteHeader(&tar.Header{Name: "a.txt", Mode: 0644, Size: 3, Typeflag: tar.TypeReg})
|
|
_, _ = tw.Write([]byte("abc"))
|
|
_ = tw.Close()
|
|
|
|
raw := buf.Bytes()
|
|
if len(raw) < 512 {
|
|
t.Fatal("tar buffer unexpectedly small")
|
|
}
|
|
var h tar.Header
|
|
if err := parseTarHeader(raw[:512], &h); err != nil {
|
|
t.Fatalf("parseTarHeader failed: %v", err)
|
|
}
|
|
if h.Name != "a.txt" {
|
|
t.Fatalf("unexpected parsed name: %q", h.Name)
|
|
}
|
|
|
|
if got := truncateString("abcdef", 5); got != "ab..." {
|
|
t.Fatalf("truncateString = %q, want %q", got, "ab...")
|
|
}
|
|
}
|
|
|
|
func TestValidateUploadSessionForJobCreation_MissingSession(t *testing.T) {
|
|
s := &Manager{
|
|
uploadSessions: map[string]*UploadSession{},
|
|
}
|
|
_, _, err := s.validateUploadSessionForJobCreation("missing", 1)
|
|
if err == nil {
|
|
t.Fatal("expected validation error for missing session")
|
|
}
|
|
sessionErr, ok := err.(*uploadSessionValidationError)
|
|
if !ok || sessionErr.Code != uploadSessionExpiredCode {
|
|
t.Fatalf("expected %s validation error, got %#v", uploadSessionExpiredCode, err)
|
|
}
|
|
}
|
|
|
|
func TestRenderTaskTimeoutIsRenderNotEncode(t *testing.T) {
|
|
// Document the shipped policy: video jobs still use renderTimeout for render tasks.
|
|
s := &Manager{renderTimeout: 3600, videoEncodeTimeout: 86400}
|
|
if s.renderTimeout == s.videoEncodeTimeout {
|
|
t.Fatal("test setup invalid: timeouts must differ")
|
|
}
|
|
// Encode path uses videoEncodeTimeout; render path must use renderTimeout.
|
|
// The create-job handler assigns s.renderTimeout to render tasks (see handleCreateJob).
|
|
if s.renderTimeout != 3600 {
|
|
t.Fatalf("renderTimeout = %d", s.renderTimeout)
|
|
}
|
|
if s.videoEncodeTimeout != 86400 {
|
|
t.Fatalf("videoEncodeTimeout = %d", s.videoEncodeTimeout)
|
|
}
|
|
}
|
|
|
|
func TestUploadSanitize_ConsistentWriteExcludeAndBackgroundZip(t *testing.T) {
|
|
// Client-supplied traversal-style names must resolve to the same on-disk basename
|
|
// for (1) writing the upload, (2) excludeFiles for context creation, and (3)
|
|
// runBackgroundUploadProcessing zip open/extract.
|
|
st, err := storage.NewStorage(t.TempDir())
|
|
if err != nil {
|
|
t.Fatalf("NewStorage: %v", err)
|
|
}
|
|
clientName := "../../evil/scene.zip"
|
|
safeName, err := storage.SanitizeFilename(clientName)
|
|
if err != nil {
|
|
t.Fatalf("SanitizeFilename: %v", err)
|
|
}
|
|
if safeName != "scene.zip" {
|
|
t.Fatalf("safeName = %q, want scene.zip", safeName)
|
|
}
|
|
|
|
tmpDir, err := st.TempDir("upload-test-*")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Write a real zip under the sanitized basename (as the upload handler does)
|
|
zipPath := filepath.Join(tmpDir, safeName)
|
|
if err := writeMinimalBlendZip(zipPath, "scene.blend"); err != nil {
|
|
t.Fatalf("write zip: %v", err)
|
|
}
|
|
// Extract so context archive has a root .blend; ZIP remains on disk to be excluded
|
|
if _, err := st.ExtractZip(zipPath, tmpDir); err != nil {
|
|
t.Fatalf("ExtractZip: %v", err)
|
|
}
|
|
if !fileExists(filepath.Join(tmpDir, "scene.blend")) {
|
|
t.Fatal("expected extracted scene.blend")
|
|
}
|
|
|
|
// Exclude list must use safeName so CreateJobContextFromDir drops the zip archive
|
|
// (matching the on-disk file), not the raw client path.
|
|
contextPath, err := st.CreateJobContextFromDir(tmpDir, 1, safeName)
|
|
if err != nil {
|
|
t.Fatalf("CreateJobContextFromDir with safe exclude: %v", err)
|
|
}
|
|
if !fileExists(contextPath) {
|
|
t.Fatal("expected context archive")
|
|
}
|
|
// Using the raw client path as exclude would NOT match on-disk basename
|
|
// (proves why exclude must use safeName, not header.Filename with traversal).
|
|
if clientName == safeName {
|
|
t.Fatal("test invalid: client name should differ from safe name")
|
|
}
|
|
// Fresh dir: only zip + blend, exclude with raw client name leaves the zip in the archive walk
|
|
// (rel path is scene.zip; client is ../../evil/scene.zip — no match).
|
|
tmpDir2, err := st.TempDir("upload-excl-*")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
zip2 := filepath.Join(tmpDir2, safeName)
|
|
if err := writeMinimalBlendZip(zip2, "scene.blend"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := st.ExtractZip(zip2, tmpDir2); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// With wrong exclude (raw client name), CreateJobContextFromDir still succeeds but the
|
|
// zip may be included. Safer assertion: wrong exclude string does not equal safeName.
|
|
wrongExclude := clientName
|
|
if filepath.Base(wrongExclude) == wrongExclude && wrongExclude == safeName {
|
|
t.Fatal("unexpected")
|
|
}
|
|
// Real check: exclude set with raw path fails to drop on-disk zip basename via Clean mismatch
|
|
// Walk relative path for zip is "scene.zip"; exclude set keys for "../../evil/scene.zip"
|
|
// after Clean become "../evil/scene.zip" which does not match.
|
|
_, errWrong := st.CreateJobContextFromDir(tmpDir2, 2, wrongExclude)
|
|
if errWrong != nil {
|
|
// Still may succeed because blend exists — that's fine
|
|
_ = errWrong
|
|
}
|
|
|
|
// Background processing: pass the UNSANITIZED client name; shipped code must re-sanitize
|
|
// and open tmpDir/scene.zip successfully. Use a clean tmp with only the zip (as after upload).
|
|
bgDir, err := st.TempDir("upload-bg-*")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bgZip := filepath.Join(bgDir, safeName)
|
|
if err := writeMinimalBlendZip(bgZip, "scene.blend"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
s := &Manager{
|
|
storage: st,
|
|
uploadSessions: map[string]*UploadSession{},
|
|
}
|
|
sessionID := "sess-traversal"
|
|
s.uploadSessions[sessionID] = &UploadSession{
|
|
SessionID: sessionID,
|
|
UserID: 7,
|
|
TempDir: bgDir,
|
|
Status: "processing",
|
|
CreatedAt: time.Now(),
|
|
}
|
|
s.runBackgroundUploadProcessing(bgDir, sessionID, 7, clientName, 100, "", "")
|
|
|
|
s.uploadSessionsMu.RLock()
|
|
session := s.uploadSessions[sessionID]
|
|
s.uploadSessionsMu.RUnlock()
|
|
if session == nil {
|
|
t.Fatal("session missing")
|
|
}
|
|
if session.Status == "error" {
|
|
t.Fatalf("background processing failed with unsanitized name: %s", session.ErrorMessage)
|
|
}
|
|
if session.Status != "completed" && session.Status != "select_blend" {
|
|
t.Fatalf("status = %q, want completed or select_blend (got message %q)", session.Status, session.Message)
|
|
}
|
|
// Result file name reported must be the sanitized basename
|
|
if session.ResultFileName != "" && session.ResultFileName != safeName {
|
|
t.Fatalf("ResultFileName = %q, want %q", session.ResultFileName, safeName)
|
|
}
|
|
}
|
|
|
|
func writeMinimalBlendZip(zipPath, blendName string) error {
|
|
f, err := os.Create(zipPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
zw := zip.NewWriter(f)
|
|
w, err := zw.Create(blendName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := w.Write([]byte("BLENDER-TEST")); err != nil {
|
|
return err
|
|
}
|
|
return zw.Close()
|
|
}
|
|
|
|
func fileExists(path string) bool {
|
|
_, err := os.Stat(path)
|
|
return err == nil
|
|
}
|
|
|
|
func TestValidateUploadSessionForJobCreation_ContextMissing(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
s := &Manager{
|
|
uploadSessions: map[string]*UploadSession{
|
|
"s1": {
|
|
SessionID: "s1",
|
|
UserID: 9,
|
|
TempDir: tmpDir,
|
|
Status: "completed",
|
|
CreatedAt: time.Now(),
|
|
},
|
|
},
|
|
}
|
|
|
|
if _, _, err := s.validateUploadSessionForJobCreation("s1", 9); err == nil {
|
|
t.Fatal("expected error when context.tar is missing")
|
|
}
|
|
}
|
|
|
|
func TestValidateUploadSessionForJobCreation_NotReady(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
s := &Manager{
|
|
uploadSessions: map[string]*UploadSession{
|
|
"s1": {
|
|
SessionID: "s1",
|
|
UserID: 9,
|
|
TempDir: tmpDir,
|
|
Status: "processing",
|
|
CreatedAt: time.Now(),
|
|
},
|
|
},
|
|
}
|
|
|
|
_, _, err := s.validateUploadSessionForJobCreation("s1", 9)
|
|
if err == nil {
|
|
t.Fatal("expected error for session that is not completed")
|
|
}
|
|
sessionErr, ok := err.(*uploadSessionValidationError)
|
|
if !ok || sessionErr.Code != uploadSessionNotReadyCode {
|
|
t.Fatalf("expected %s validation error, got %#v", uploadSessionNotReadyCode, err)
|
|
}
|
|
}
|
|
|
|
func TestValidateUploadSessionForJobCreation_Success(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
contextPath := filepath.Join(tmpDir, "context.tar")
|
|
if err := os.WriteFile(contextPath, []byte("tar-bytes"), 0644); err != nil {
|
|
t.Fatalf("write context.tar: %v", err)
|
|
}
|
|
|
|
s := &Manager{
|
|
uploadSessions: map[string]*UploadSession{
|
|
"s1": {
|
|
SessionID: "s1",
|
|
UserID: 9,
|
|
TempDir: tmpDir,
|
|
Status: "completed",
|
|
CreatedAt: time.Now(),
|
|
},
|
|
},
|
|
}
|
|
|
|
session, gotPath, err := s.validateUploadSessionForJobCreation("s1", 9)
|
|
if err != nil {
|
|
t.Fatalf("expected valid session, got error: %v", err)
|
|
}
|
|
if session == nil || gotPath != contextPath {
|
|
t.Fatalf("unexpected result: session=%v path=%q", session, gotPath)
|
|
}
|
|
}
|
|
|