Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
491 lines
16 KiB
Go
491 lines
16 KiB
Go
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)
|
|
}
|
|
}
|