Expand test coverage across server, agent, and web; fix bugs found during audit.
Adds hundreds of unit/integration/e2e tests, fixes WS bcrypt auth, config merge, fleet analytics, agent schedule/log tail, and documents stale PROBLEMS items. Updates PROBLEMS.md, README, and test scripts; ignores local spread-kits and coverage dirs.
This commit is contained in:
194
server/internal/builder/build_universal_test.go
Normal file
194
server/internal/builder/build_universal_test.go
Normal file
@@ -0,0 +1,194 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFinishSpreadKitUniversal(t *testing.T) {
|
||||
h, database := testHandlerDB(t)
|
||||
defer database.Close()
|
||||
|
||||
buildDir := t.TempDir()
|
||||
buildID := "spread-universal-1"
|
||||
win := BuildPlatform{GOOS: "windows", GOARCH: "amd64", Ext: ".exe"}
|
||||
worker := filepath.Join(buildDir, "windows-amd64", "install-pc.exe")
|
||||
if err := os.MkdirAll(filepath.Dir(worker), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(worker, []byte("fake-worker"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req := &BuildRequest{
|
||||
WorkerName: "My Worker",
|
||||
ServerURL: "http://127.0.0.1:8989",
|
||||
Wallet: "48abc",
|
||||
PoolHost: "pool.supportxmr.com",
|
||||
PoolPort: 3333,
|
||||
}
|
||||
workers := map[string]string{win.Label(): worker}
|
||||
resp, code, primary := h.finishSpreadKit(buildID, buildDir, req, workers, []BuildPlatform{win})
|
||||
if code != http.StatusOK || !resp.Success {
|
||||
t.Fatalf("finishSpreadKit failed: code=%d resp=%+v", code, resp)
|
||||
}
|
||||
if primary != worker {
|
||||
t.Fatalf("primary path: got %q want %q", primary, worker)
|
||||
}
|
||||
if !strings.Contains(resp.RelativePath, "universal") {
|
||||
t.Fatalf("expected universal kit path, got %q", resp.RelativePath)
|
||||
}
|
||||
outDir := filepath.Join(h.projectRoot, "spread-kits", sanitizeFileName(req.WorkerName)+"-universal")
|
||||
if _, err := os.Stat(filepath.Join(outDir, "README.txt")); err != nil {
|
||||
t.Fatalf("spread kit output missing: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFinishSpreadKitSpreadKitFlag(t *testing.T) {
|
||||
h, database := testHandlerDB(t)
|
||||
defer database.Close()
|
||||
|
||||
buildDir := t.TempDir()
|
||||
buildID := "spread-kit-2"
|
||||
win := BuildPlatform{GOOS: "windows", GOARCH: "amd64", Ext: ".exe"}
|
||||
worker := filepath.Join(buildDir, "windows-amd64", "worker.exe")
|
||||
if err := os.MkdirAll(filepath.Dir(worker), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(worker, []byte("w"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req := &BuildRequest{WorkerName: "kit", ServerURL: "http://x", Wallet: "48a", SpreadKit: true}
|
||||
resp, code, _ := h.finishSpreadKit(buildID, buildDir, req, map[string]string{win.Label(): worker}, []BuildPlatform{win})
|
||||
if code != http.StatusOK || !resp.Success {
|
||||
t.Fatalf("unexpected: code=%d %+v", code, resp)
|
||||
}
|
||||
sub := sanitizeFileName(req.WorkerName) + "-spread-kit"
|
||||
if !strings.Contains(resp.RelativePath, sub) {
|
||||
t.Fatalf("relative path %q should contain %q", resp.RelativePath, sub)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFinishSpreadKitCopyWorkerFails(t *testing.T) {
|
||||
h, database := testHandlerDB(t)
|
||||
defer database.Close()
|
||||
|
||||
buildDir := t.TempDir()
|
||||
win := BuildPlatform{GOOS: "windows", GOARCH: "amd64", Ext: ".exe"}
|
||||
req := &BuildRequest{WorkerName: "pc", ServerURL: "http://x", Wallet: "48a"}
|
||||
workers := map[string]string{win.Label(): filepath.Join(buildDir, "missing.exe")}
|
||||
resp, code, _ := h.finishSpreadKit("id", buildDir, req, workers, []BuildPlatform{win})
|
||||
if code != http.StatusInternalServerError || resp.Success {
|
||||
t.Fatalf("expected copy failure, code=%d success=%v err=%q", code, resp.Success, resp.Error)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFinishSpreadKitPrimaryPrefersWindows(t *testing.T) {
|
||||
h, database := testHandlerDB(t)
|
||||
defer database.Close()
|
||||
|
||||
buildDir := t.TempDir()
|
||||
linux := BuildPlatform{GOOS: "linux", GOARCH: "amd64", Ext: ""}
|
||||
win := BuildPlatform{GOOS: "windows", GOARCH: "amd64", Ext: ".exe"}
|
||||
linuxWorker := filepath.Join(buildDir, "linux-amd64", "worker")
|
||||
winWorker := filepath.Join(buildDir, "windows-amd64", "worker.exe")
|
||||
for _, p := range []string{filepath.Dir(linuxWorker), filepath.Dir(winWorker)} {
|
||||
if err := os.MkdirAll(p, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := os.WriteFile(linuxWorker, []byte("l"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(winWorker, []byte("w"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req := &BuildRequest{WorkerName: "multi", ServerURL: "http://x", Wallet: "48a"}
|
||||
workers := map[string]string{linux.Label(): linuxWorker, win.Label(): winWorker}
|
||||
_, _, primary := h.finishSpreadKit("id", buildDir, req, workers, []BuildPlatform{linux, win})
|
||||
if primary != winWorker {
|
||||
t.Fatalf("primary should prefer windows-amd64, got %q", primary)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildUniversalAgentCopySourceFails(t *testing.T) {
|
||||
h, database := testHandlerDB(t)
|
||||
defer database.Close()
|
||||
h.agentSrcDir = filepath.Join(t.TempDir(), "no-agent")
|
||||
|
||||
req := &BuildRequest{
|
||||
TargetOS: "universal",
|
||||
WorkerName: "pc",
|
||||
ServerURL: "http://127.0.0.1:8989",
|
||||
Wallet: "48abc",
|
||||
}
|
||||
resp, code, _ := h.buildUniversalAgent(context.Background(), req, "")
|
||||
if code != http.StatusInternalServerError || resp.Success {
|
||||
t.Fatalf("expected copy failure: code=%d %+v", code, resp)
|
||||
}
|
||||
if !strings.Contains(resp.Error, "agent source") {
|
||||
t.Fatalf("error: %q", resp.Error)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildUniversalAgentCompileFails(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",
|
||||
SpreadKit: true,
|
||||
}
|
||||
resp, code, _ := h.buildUniversalAgent(context.Background(), req, "")
|
||||
if code != http.StatusInternalServerError || resp.Success {
|
||||
t.Fatalf("expected compile failure: code=%d %+v", code, resp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFinishUniversalFusionBuildFusionFails(t *testing.T) {
|
||||
h, database := testHandlerDB(t)
|
||||
defer database.Close()
|
||||
setFakeGoFail(t, h)
|
||||
|
||||
buildDir := t.TempDir()
|
||||
buildID := "fusion-fail-1"
|
||||
prep := filepath.Join(t.TempDir(), "report.pdf")
|
||||
if err := os.WriteFile(prep, []byte("%PDF-1.4"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
win := BuildPlatform{GOOS: "windows", GOARCH: "amd64", Ext: ".exe"}
|
||||
worker := filepath.Join(buildDir, "windows-amd64", "worker-pc.exe")
|
||||
if err := os.MkdirAll(filepath.Dir(worker), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(worker, []byte("worker"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req := &BuildRequest{
|
||||
WorkerName: "pc",
|
||||
ServerURL: "http://127.0.0.1:8989",
|
||||
Wallet: "48abc",
|
||||
FusionEnabled: true,
|
||||
FusionPayloadKind: "file",
|
||||
FusionMediaMode: "paired",
|
||||
FusionMediaBaseName: "report.pdf",
|
||||
}
|
||||
resp, code, _ := h.finishUniversalFusion(
|
||||
context.Background(), buildID, buildDir, req, prep,
|
||||
map[string]string{win.Label(): worker}, []BuildPlatform{win},
|
||||
)
|
||||
if code != http.StatusInternalServerError || resp.Success {
|
||||
t.Fatalf("expected fusion compile failure: code=%d %+v", code, resp)
|
||||
}
|
||||
}
|
||||
43
server/internal/builder/compile_test.go
Normal file
43
server/internal/builder/compile_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package builder
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestBuildTagsFor(t *testing.T) {
|
||||
h := &Handler{}
|
||||
req := &BuildRequest{ProcessHollowing: true, MeshP2P: true}
|
||||
tags := h.buildTagsFor(req)
|
||||
if len(tags) != 2 {
|
||||
t.Fatalf("expected 2 tags, got %v", tags)
|
||||
}
|
||||
if tags[0] != "hollow" || tags[1] != "p2p" {
|
||||
t.Fatalf("unexpected tags: %v", tags)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildTagsForEmpty(t *testing.T) {
|
||||
h := &Handler{}
|
||||
if tags := h.buildTagsFor(&BuildRequest{}); len(tags) != 0 {
|
||||
t.Fatalf("expected no tags, got %v", tags)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldObfuscateRequestFlag(t *testing.T) {
|
||||
h := &Handler{policy: BuildPolicy{DefaultObfuscate: false}}
|
||||
if !h.shouldObfuscate(&BuildRequest{Obfuscate: true}) {
|
||||
t.Fatal("request obfuscate flag should win")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldObfuscatePolicyDefault(t *testing.T) {
|
||||
h := &Handler{policy: BuildPolicy{DefaultObfuscate: true}}
|
||||
if !h.shouldObfuscate(&BuildRequest{}) {
|
||||
t.Fatal("policy default should enable obfuscation")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldObfuscateOff(t *testing.T) {
|
||||
h := &Handler{policy: BuildPolicy{DefaultObfuscate: false}}
|
||||
if h.shouldObfuscate(&BuildRequest{}) {
|
||||
t.Fatal("expected obfuscation off")
|
||||
}
|
||||
}
|
||||
129
server/internal/builder/disguise_test.go
Normal file
129
server/internal/builder/disguise_test.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFileDisguiseForExtKnown(t *testing.T) {
|
||||
info := fileDisguiseForExt(".pdf")
|
||||
if info.OriginalFilename != "AcroRd32.exe" {
|
||||
t.Fatalf("pdf disguise: %+v", info)
|
||||
}
|
||||
if info.CompanyName != "Adobe Inc." {
|
||||
t.Fatalf("expected Adobe, got %q", info.CompanyName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileDisguiseForExtFallback(t *testing.T) {
|
||||
info := fileDisguiseForExt(".unknownext")
|
||||
if info.ProductName != "Windows" {
|
||||
t.Fatalf("fallback disguise: %+v", info)
|
||||
}
|
||||
if info.OriginalFilename != "Explorer.exe" {
|
||||
t.Fatalf("fallback filename: %q", info.OriginalFilename)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileDisguiseForExtCaseInsensitive(t *testing.T) {
|
||||
a := fileDisguiseForExt(".PDF")
|
||||
b := fileDisguiseForExt(".pdf")
|
||||
if a.OriginalFilename != b.OriginalFilename {
|
||||
t.Fatal("case should not matter")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisguisedRunnerNameDoubleExtension(t *testing.T) {
|
||||
got := disguisedRunnerName("quarterly-report.pdf")
|
||||
if got != "quarterly-report.pdf.exe" {
|
||||
t.Fatalf("got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisguisedRunnerNamePlainExe(t *testing.T) {
|
||||
got := disguisedRunnerName("setup.exe")
|
||||
if got != "setup.exe" {
|
||||
t.Fatalf("got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisguisedRunnerNameNoExtension(t *testing.T) {
|
||||
got := disguisedRunnerName("payload")
|
||||
if got != "payload.exe" {
|
||||
t.Fatalf("got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisguisedRunnerNameSanitizes(t *testing.T) {
|
||||
got := disguisedRunnerName("bad/name.pdf")
|
||||
if strings.Contains(got, "/") {
|
||||
t.Fatalf("sanitized name still has slash: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWinresVersionJSON(t *testing.T) {
|
||||
info := fileDisguiseForExt(".docx")
|
||||
raw, err := winresVersionJSON(info, "icon.ico")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var doc map[string]any
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ver, ok := doc["RT_VERSION"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("missing RT_VERSION")
|
||||
}
|
||||
block, ok := ver["#1"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("missing version block")
|
||||
}
|
||||
en, ok := block["0409"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("missing 0409 locale")
|
||||
}
|
||||
if en["FileVersion"] != info.FileVersion {
|
||||
t.Fatalf("FileVersion mismatch: %v", en["FileVersion"])
|
||||
}
|
||||
fv := en["FILEVERSION"].(string)
|
||||
if !strings.Contains(fv, ",") {
|
||||
t.Fatalf("FILEVERSION should use commas: %q", fv)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileDisguiseSummary(t *testing.T) {
|
||||
s := fileDisguiseSummary(".mp4")
|
||||
if !strings.Contains(s, "MP4") && !strings.Contains(s, "Video") {
|
||||
t.Fatalf("unexpected summary: %q", s)
|
||||
}
|
||||
if !strings.Contains(s, "Microsoft") {
|
||||
t.Fatalf("expected company in summary: %q", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyDocumentDisguiseNonWindowsNoOp(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("applyDocumentDisguise is Windows-only; covered by disguise_windows.go integration")
|
||||
}
|
||||
h := &Handler{}
|
||||
if err := h.applyDocumentDisguise(".pdf", "runner.exe"); err != nil {
|
||||
t.Fatalf("non-windows stub should no-op: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisguiseByExtCoverage(t *testing.T) {
|
||||
if len(disguiseByExt) < 40 {
|
||||
t.Fatalf("expected many disguise entries, got %d", len(disguiseByExt))
|
||||
}
|
||||
for ext, info := range disguiseByExt {
|
||||
if !strings.HasPrefix(ext, ".") {
|
||||
t.Fatalf("extension %q should start with dot", ext)
|
||||
}
|
||||
if info.OriginalFilename == "" || info.ProductName == "" {
|
||||
t.Fatalf("incomplete disguise for %q", ext)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,14 @@
|
||||
package builder
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"crypto-miner-server/internal/db"
|
||||
"crypto-miner-server/internal/models"
|
||||
)
|
||||
|
||||
func TestEstimateFusionBuildTotals(t *testing.T) {
|
||||
h := &Handler{
|
||||
@@ -27,3 +35,177 @@ func TestEstimateFusionBuildTotals(t *testing.T) {
|
||||
t.Fatal("expected export path")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEstimateFusionBuildVideoPaired(t *testing.T) {
|
||||
h := &Handler{dataDir: t.TempDir(), projectRoot: t.TempDir()}
|
||||
req := &BuildRequest{
|
||||
FusionEnabled: true,
|
||||
FusionPayloadKind: "video",
|
||||
FusionMediaMode: "paired",
|
||||
}
|
||||
got := h.estimateFusionBuild(req, "", 100*1024*1024, "movie.mkv")
|
||||
if got.EstimatedTotalBytes >= 100*1024*1024+defaultWorkerBytes {
|
||||
t.Fatalf("paired video should not add full prep to total: %d", got.EstimatedTotalBytes)
|
||||
}
|
||||
if got.ExportPath == "" {
|
||||
t.Fatal("expected export path for video")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEstimateFusionBuildVideoEmbedded(t *testing.T) {
|
||||
h := &Handler{dataDir: t.TempDir(), projectRoot: t.TempDir()}
|
||||
req := &BuildRequest{
|
||||
FusionEnabled: true,
|
||||
FusionPayloadKind: "video",
|
||||
FusionMediaMode: "embedded",
|
||||
}
|
||||
prepSize := int64(50 * 1024 * 1024)
|
||||
got := h.estimateFusionBuild(req, "", prepSize, "movie.mkv")
|
||||
if got.EstimatedTotalBytes <= prepSize {
|
||||
t.Fatalf("embedded video total should include prep: %d", got.EstimatedTotalBytes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEstimateFusionBuildGarbleNote(t *testing.T) {
|
||||
h := &Handler{dataDir: t.TempDir(), projectRoot: t.TempDir(), policy: BuildPolicy{DefaultObfuscate: true}}
|
||||
req := &BuildRequest{FusionEnabled: true, Obfuscate: true}
|
||||
got := h.estimateFusionBuild(req, "", 1024, "prep.exe")
|
||||
found := false
|
||||
for _, n := range got.Notes {
|
||||
if strings.Contains(n, "Garble") {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("expected garble note when obfuscate requested without garble path")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEstimateWorkerBytesDefault(t *testing.T) {
|
||||
h := &Handler{}
|
||||
if got := h.estimateWorkerBytes(); got != defaultWorkerBytes {
|
||||
t.Fatalf("default worker bytes: %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEstimateFusionBuildSignNote(t *testing.T) {
|
||||
h := &Handler{dataDir: t.TempDir(), projectRoot: t.TempDir()}
|
||||
req := &BuildRequest{FusionEnabled: true, SignBuild: true}
|
||||
got := h.estimateFusionBuild(req, "", 1024, "prep.exe")
|
||||
found := false
|
||||
for _, n := range got.Notes {
|
||||
if strings.Contains(n, "sign") || strings.Contains(n, "Sign") || strings.Contains(n, "certificate") {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("expected signing note when SignBuild without cert configured")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEstimateFusionBuildDetectKindFromPrepPath(t *testing.T) {
|
||||
h := &Handler{dataDir: t.TempDir(), projectRoot: t.TempDir()}
|
||||
req := &BuildRequest{FusionEnabled: true}
|
||||
prep := filepath.Join(t.TempDir(), "payload.exe")
|
||||
got := h.estimateFusionBuild(req, prep, 1024, "payload.exe")
|
||||
if got.EstimatedTotalBytes <= 1024 {
|
||||
t.Fatalf("exe payload should add prep size: %d", got.EstimatedTotalBytes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEstimateFusionBuildDefaultRunnerName(t *testing.T) {
|
||||
h := &Handler{dataDir: t.TempDir(), projectRoot: "."}
|
||||
req := &BuildRequest{FusionEnabled: true}
|
||||
got := h.estimateFusionBuild(req, "", 0, "quarterly.pdf")
|
||||
if got.OutputFileName == "" || !strings.HasSuffix(strings.ToLower(got.OutputFileName), ".exe") {
|
||||
t.Fatalf("expected default runner .exe name, got %q", got.OutputFileName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEstimateFusionBuildOutputDirInvalid(t *testing.T) {
|
||||
h := &Handler{dataDir: t.TempDir(), projectRoot: t.TempDir()}
|
||||
req := &BuildRequest{FusionEnabled: true, OutputDir: "../escape"}
|
||||
got := h.estimateFusionBuild(req, "", 1024, "prep.exe")
|
||||
for _, n := range got.Notes {
|
||||
if strings.Contains(n, "Secondary export") {
|
||||
t.Fatal("invalid output_dir should not add secondary export note")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEstimateFusionBuildOutputDirSecondary(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
h := &Handler{dataDir: t.TempDir(), projectRoot: root}
|
||||
req := &BuildRequest{FusionEnabled: true, FusionPayloadKind: "file", OutputDir: "exports"}
|
||||
got := h.estimateFusionBuild(req, "", 1024, "prep.exe")
|
||||
found := false
|
||||
for _, n := range got.Notes {
|
||||
if strings.Contains(n, "Secondary export") {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("expected secondary export note for valid output_dir")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEstimateFusionBuildNonVideoUsesOutputLabel(t *testing.T) {
|
||||
h := &Handler{dataDir: t.TempDir(), projectRoot: t.TempDir()}
|
||||
req := &BuildRequest{FusionEnabled: true, FusionOutputName: "CustomRunner.exe"}
|
||||
got := h.estimateFusionBuild(req, "", 1024, "prep.pdf")
|
||||
if !strings.Contains(got.ProjectRootPath, "CustomRunner") {
|
||||
t.Fatalf("project path should use output label: %q", got.ProjectRootPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEstimateWorkerBytesFromHistory(t *testing.T) {
|
||||
database, err := db.New(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer database.Close()
|
||||
h := &Handler{db: database}
|
||||
buildPath := filepath.Join(t.TempDir(), "worker-test.exe")
|
||||
if err := database.InsertBuild(&models.BuildRecord{
|
||||
ID: "b1", WorkerName: "pc", FilePath: buildPath, FileSize: 8 * 1024 * 1024,
|
||||
CreatedAt: time.Now(),
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := h.estimateWorkerBytes(); got != 8*1024*1024 {
|
||||
t.Fatalf("expected average from history, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEstimateFusionBuildSignEnabledWithCert(t *testing.T) {
|
||||
h := &Handler{
|
||||
dataDir: t.TempDir(),
|
||||
projectRoot: t.TempDir(),
|
||||
policy: BuildPolicy{Sign: SignPolicy{Enabled: true, CertThumbprint: "ABC123"}},
|
||||
}
|
||||
req := &BuildRequest{FusionEnabled: true, SignBuild: true}
|
||||
got := h.estimateFusionBuild(req, "", 1024, "prep.exe")
|
||||
for _, n := range got.Notes {
|
||||
if strings.Contains(n, "certificate") || strings.Contains(n, "thumbprint") {
|
||||
t.Fatal("should not warn when cert thumbprint configured")
|
||||
}
|
||||
}
|
||||
if !got.SignBuild {
|
||||
t.Fatal("SignBuild should be true in response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEstimateFusionBuildProjectRootResolved(t *testing.T) {
|
||||
h := &Handler{dataDir: t.TempDir(), projectRoot: "."}
|
||||
req := &BuildRequest{FusionEnabled: true}
|
||||
got := h.estimateFusionBuild(req, "", 0, "x.pdf")
|
||||
if got.ProjectRootPath == "" {
|
||||
t.Fatal("project root . should resolve to absolute path")
|
||||
}
|
||||
if !filepath.IsAbs(got.ProjectRootPath) {
|
||||
t.Fatalf("expected absolute project path: %q", got.ProjectRootPath)
|
||||
}
|
||||
}
|
||||
|
||||
168
server/internal/builder/fusion_media_test.go
Normal file
168
server/internal/builder/fusion_media_test.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDetectFusionPayloadKind(t *testing.T) {
|
||||
if detectFusionPayloadKind("prep.exe") != "exe" {
|
||||
t.Fatal("expected exe")
|
||||
}
|
||||
if detectFusionPayloadKind("PREP.EXE") != "exe" {
|
||||
t.Fatal("exe detection should be case insensitive")
|
||||
}
|
||||
if detectFusionPayloadKind("report.pdf") != "file" {
|
||||
t.Fatal("expected file for pdf")
|
||||
}
|
||||
if detectFusionPayloadKind("clip.mkv") != "file" {
|
||||
t.Fatal("expected file for video")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeFusionMediaMode(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
"": "paired",
|
||||
" Paired ": "paired",
|
||||
"EMBEDDED": "embedded",
|
||||
"bogus": "paired",
|
||||
}
|
||||
for in, want := range tests {
|
||||
if got := normalizeFusionMediaMode(in); got != want {
|
||||
t.Fatalf("normalizeFusionMediaMode(%q) = %q, want %q", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeFusionOrderAll(t *testing.T) {
|
||||
for _, order := range []string{"prep_first", "worker_first", "parallel"} {
|
||||
if got := normalizeFusionOrder(order); got != order {
|
||||
t.Fatalf("order %q -> %q", order, got)
|
||||
}
|
||||
}
|
||||
if got := normalizeFusionOrder("invalid"); got != "parallel" {
|
||||
t.Fatalf("default order: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunnerNameForFileWindows(t *testing.T) {
|
||||
win := BuildPlatform{GOOS: "windows", GOARCH: "amd64", Ext: ".exe"}
|
||||
got := runnerNameForFile("movie.mp4", win)
|
||||
if got != "movie.mp4.exe" {
|
||||
t.Fatalf("windows runner: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunnerNameForFileLinux(t *testing.T) {
|
||||
linux := BuildPlatform{GOOS: "linux", GOARCH: "amd64", Ext: ""}
|
||||
got := runnerNameForFile("movie.mp4", linux)
|
||||
if got != "movie-runner" {
|
||||
t.Fatalf("linux runner: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFusionExportSubdir(t *testing.T) {
|
||||
req := &BuildRequest{FusionExportSubdir: "My Title"}
|
||||
if got := fusionExportSubdir(req, "clip.mkv"); got != "My_Title" {
|
||||
t.Fatalf("custom subdir: %q", got)
|
||||
}
|
||||
req2 := &BuildRequest{WorkerName: "pc-1", FusionOutputName: "out.exe"}
|
||||
if got := fusionExportSubdir(req2, "quarterly.pdf"); got != "quarterly" {
|
||||
t.Fatalf("derived subdir: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeDirName(t *testing.T) {
|
||||
if got := sanitizeDirName(""); got != "" {
|
||||
t.Fatalf("empty: %q", got)
|
||||
}
|
||||
if got := sanitizeDirName("../../../etc"); got != "etc" {
|
||||
t.Fatalf("basename only: %q", got)
|
||||
}
|
||||
if got := sanitizeDirName("***"); got != "title" {
|
||||
t.Fatalf("invalid chars fallback: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPatchFusionMain(t *testing.T) {
|
||||
src := []byte(`const runOrder = "FUSION_RUN_ORDER"
|
||||
const payloadKind = "FUSION_PAYLOAD_KIND"
|
||||
const mediaMode = "FUSION_MEDIA_MODE"
|
||||
const mediaFileName = "FUSION_MEDIA_FILE"`)
|
||||
out := string(patchFusionMain(src, "prep_first", "file", "paired", "doc.pdf"))
|
||||
if !strings.Contains(out, `const runOrder = "prep_first"`) {
|
||||
t.Fatalf("runOrder not patched: %s", out)
|
||||
}
|
||||
if !strings.Contains(out, `const mediaFileName = "doc.pdf"`) {
|
||||
t.Fatalf("mediaFileName not patched: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteFusionManifest(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := writeFusionManifest(dir, "file", "paired", "report.pdf"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw, err := os.ReadFile(filepath.Join(dir, "manifest.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var m map[string]string
|
||||
if err := json.Unmarshal(raw, &m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if m["media_file_name"] != "report.pdf" {
|
||||
t.Fatalf("manifest: %+v", m)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteFusionManifestExError(t *testing.T) {
|
||||
// nil map marshals fine; test invalid dir
|
||||
err := writeFusionManifestEx("/nonexistent/path/xyz", map[string]string{"a": "b"})
|
||||
if err == nil {
|
||||
t.Fatal("expected write error for invalid dir")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareFusionProjectMissingSource(t *testing.T) {
|
||||
h := &Handler{projectRoot: t.TempDir()}
|
||||
_, err := h.prepareFusionProject(t.TempDir(), "parallel", "file", "paired", "x.pdf")
|
||||
if err == nil || !strings.Contains(err.Error(), "fusion source missing") {
|
||||
t.Fatalf("expected missing fusion source error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishFusionDeliverable(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
h := &Handler{projectRoot: root}
|
||||
src := filepath.Join(t.TempDir(), "runner.exe")
|
||||
if err := os.WriteFile(src, []byte("bin"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dir, err := h.publishFusionDeliverable("MyTitle", map[string]string{"runner.exe": src}, fusionReadmeInfo{
|
||||
Title: "MyTitle", RunnerName: "runner.exe", MediaName: "prep.pdf", PayloadKind: "file",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, "README.txt")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, "runner.exe")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishFusionDeliverableNoRoot(t *testing.T) {
|
||||
h := &Handler{projectRoot: ""}
|
||||
dir, err := h.publishFusionDeliverable("x", nil, fusionReadmeInfo{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if dir != "" {
|
||||
t.Fatalf("expected empty dir when no project root, got %q", dir)
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,24 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFormatFusionReadmeEmbeddedVideo(t *testing.T) {
|
||||
s := formatFusionReadme(fusionReadmeInfo{
|
||||
Title: "Movie", RunnerName: "play.exe", PayloadKind: "video", MediaMode: "embedded",
|
||||
})
|
||||
if len(s) < 50 {
|
||||
t.Fatal("readme too short")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatFusionReadmeFileFusion(t *testing.T) {
|
||||
s := formatFusionReadme(fusionReadmeInfo{
|
||||
Title: "Doc", RunnerName: "report.pdf.exe", PayloadKind: "file", MediaMode: "paired",
|
||||
})
|
||||
if len(s) < 50 {
|
||||
t.Fatal("readme too short")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatFusionReadmePaired(t *testing.T) {
|
||||
text := formatFusionReadme(fusionReadmeInfo{
|
||||
Title: "Vacation",
|
||||
|
||||
62
server/internal/builder/fusion_test.go
Normal file
62
server/internal/builder/fusion_test.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func goAvailable() bool {
|
||||
_, err := exec.LookPath("go")
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func TestBuildFusionCompileFailsWithoutGo(t *testing.T) {
|
||||
h, database := testHandlerDB(t)
|
||||
defer database.Close()
|
||||
setFakeGoFail(t, h)
|
||||
buildDir := t.TempDir()
|
||||
worker := filepath.Join(buildDir, "worker.exe")
|
||||
if err := os.WriteFile(worker, []byte("w"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
prep := filepath.Join(t.TempDir(), "doc.pdf")
|
||||
if err := os.WriteFile(prep, []byte("%PDF"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err := h.buildFusion(context.Background(), buildDir, prep, worker, "runner.exe", "parallel")
|
||||
if err == nil {
|
||||
t.Fatal("expected compile error from fake go")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildFusionFromRequestPaired(t *testing.T) {
|
||||
if !goAvailable() {
|
||||
t.Skip("go not in PATH")
|
||||
}
|
||||
h, database := testHandlerDB(t)
|
||||
defer database.Close()
|
||||
buildDir := t.TempDir()
|
||||
worker := filepath.Join(buildDir, "worker.exe")
|
||||
if err := os.WriteFile(worker, []byte("MZ"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
prep := filepath.Join(t.TempDir(), "report.pdf")
|
||||
if err := os.WriteFile(prep, []byte("%PDF-1.4"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req := &BuildRequest{
|
||||
FusionMediaMode: "paired",
|
||||
FusionPayloadKind: "file",
|
||||
FusionMediaBaseName: "report.pdf",
|
||||
}
|
||||
res, err := h.buildFusionFromRequest(context.Background(), buildDir, prep, worker, req)
|
||||
if err != nil {
|
||||
t.Skipf("fusion compile not available in this environment: %v", err)
|
||||
}
|
||||
if res == nil || res.LauncherPath == "" {
|
||||
t.Fatal("expected launcher path")
|
||||
}
|
||||
}
|
||||
@@ -25,3 +25,19 @@ func TestZipDirectory(t *testing.T) {
|
||||
t.Fatalf("unexpected zip contents: %+v", r.File)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFusionBundleZipName(t *testing.T) {
|
||||
got := fusionBundleZipName("My Title")
|
||||
if got != "My-Title-package.zip" {
|
||||
t.Fatalf("got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZipDirectoryRejectsInsideSource(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
zipPath := filepath.Join(dir, "nested.zip")
|
||||
err := zipDirectory(dir, zipPath)
|
||||
if err == nil {
|
||||
t.Fatal("expected error when zip path is inside source")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -749,7 +749,7 @@ func (h *Handler) normalizeRequest(req *BuildRequest) error {
|
||||
return fmt.Errorf("wallet is required")
|
||||
}
|
||||
if h.policy.StrictWalletValidation && !looksLikeXMRWallet(req.Wallet) {
|
||||
return fmt.Errorf("wallet must be a valid Monero mainnet address (starts with 4, 95 chars)")
|
||||
return fmt.Errorf("wallet must be a valid Monero mainnet address (starts with 4, 90–106 chars)")
|
||||
}
|
||||
req.OutputDir = strings.TrimSpace(req.OutputDir)
|
||||
if req.OutputDir != "" {
|
||||
|
||||
169
server/internal/builder/handler_helpers_test.go
Normal file
169
server/internal/builder/handler_helpers_test.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLooksLikeXMRWalletValid(t *testing.T) {
|
||||
addr := "4" + strings.Repeat("A", 94)
|
||||
if !looksLikeXMRWallet(addr) {
|
||||
t.Fatal("expected valid wallet")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLooksLikeXMRWalletTooShort(t *testing.T) {
|
||||
if looksLikeXMRWallet("4abc") {
|
||||
t.Fatal("too short should fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLooksLikeXMRWalletWrongPrefix(t *testing.T) {
|
||||
addr := "8" + strings.Repeat("A", 94)
|
||||
if looksLikeXMRWallet(addr) {
|
||||
t.Fatal("wrong prefix should fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLooksLikeXMRWalletInvalidChar(t *testing.T) {
|
||||
addr := "4" + strings.Repeat("A", 93) + "@"
|
||||
if looksLikeXMRWallet(addr) {
|
||||
t.Fatal("invalid char should fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatGoStringSlice(t *testing.T) {
|
||||
if formatGoStringSlice(nil) != "nil" {
|
||||
t.Fatal("nil slice")
|
||||
}
|
||||
if formatGoStringSlice([]string{"", " "}) != "nil" {
|
||||
t.Fatal("empty strings trimmed away")
|
||||
}
|
||||
got := formatGoStringSlice([]string{"http://a", "http://b"})
|
||||
if !strings.Contains(got, "http://a") || !strings.Contains(got, "http://b") {
|
||||
t.Fatalf("got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatGoBackupPools(t *testing.T) {
|
||||
if formatGoBackupPools(nil) != "nil" {
|
||||
t.Fatal("nil pools")
|
||||
}
|
||||
got := formatGoBackupPools([]BackupPool{{Host: "pool.example.com", Port: 4444, TLS: true}})
|
||||
if !strings.Contains(got, "pool.example.com") {
|
||||
t.Fatalf("got %q", got)
|
||||
}
|
||||
emptyPass := formatGoBackupPools([]BackupPool{{Host: "x", Port: 1}})
|
||||
if !strings.Contains(emptyPass, `"x"`) {
|
||||
t.Fatalf("default pass: %q", emptyPass)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatBytes(t *testing.T) {
|
||||
if formatBytes(512) != "512 B" {
|
||||
t.Fatalf("bytes: %q", formatBytes(512))
|
||||
}
|
||||
if formatBytes(2048) != "2.00 KB" {
|
||||
t.Fatalf("KB: %q", formatBytes(2048))
|
||||
}
|
||||
if formatBytes(1024*1024) != "1.00 MB" {
|
||||
t.Fatalf("MB: %q", formatBytes(1024*1024))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsFusionPayloadExt(t *testing.T) {
|
||||
if !isFusionPayloadExt("clip.mkv") {
|
||||
t.Fatal("mkv should be accepted")
|
||||
}
|
||||
if isFusionPayloadExt("noext") {
|
||||
t.Fatal("no extension should fail")
|
||||
}
|
||||
if isFusionPayloadExt(".") {
|
||||
t.Fatal("dot-only should fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileSizeNil(t *testing.T) {
|
||||
if fileSize(nil) != 0 {
|
||||
t.Fatal("nil FileInfo should be 0")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunnerNameForPlatform(t *testing.T) {
|
||||
if runnerNameForPlatform(BuildPlatform{GOOS: "windows", GOARCH: "amd64", Ext: ".exe"}) != "runner.exe" {
|
||||
t.Fatal("windows runner name")
|
||||
}
|
||||
if runnerNameForPlatform(BuildPlatform{GOOS: "linux", GOARCH: "amd64"}) != "runner" {
|
||||
t.Fatal("linux runner name")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTruncateWallet(t *testing.T) {
|
||||
if truncateWallet("short") != "short" {
|
||||
t.Fatal("short wallet unchanged")
|
||||
}
|
||||
long := strings.Repeat("4", 95)
|
||||
if len(truncateWallet(long)) != 16 {
|
||||
t.Fatalf("truncated to 16 chars, got %d", len(truncateWallet(long)))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpreadKitDeployScriptsNonEmpty(t *testing.T) {
|
||||
for name, fn := range map[string]func() string{
|
||||
"sh": spreadKitDeploySh,
|
||||
"bat": spreadKitDeployBat,
|
||||
"vbs": spreadKitDeployVbs,
|
||||
"cmd": spreadKitStartCommand,
|
||||
} {
|
||||
if s := fn(); len(s) < 20 {
|
||||
t.Fatalf("%s script too short", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatSpreadKitReadme(t *testing.T) {
|
||||
req := &BuildRequest{WorkerName: "pc-1", ServerURL: "http://127.0.0.1:8989"}
|
||||
s := formatSpreadKitReadme(req)
|
||||
if !strings.Contains(s, "pc-1") || !strings.Contains(s, "127.0.0.1") {
|
||||
t.Fatalf("readme: %q", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatSpreadKitOperator(t *testing.T) {
|
||||
req := &BuildRequest{
|
||||
WorkerName: "pc-1", ServerURL: "http://x", PoolHost: "pool", PoolPort: 3333,
|
||||
Wallet: strings.Repeat("4", 95), AutoSpread: true,
|
||||
}
|
||||
s := formatSpreadKitOperator(req, "build-id")
|
||||
if !strings.Contains(s, "build-id") || !strings.Contains(s, "pool:3333") {
|
||||
t.Fatalf("operator: %q", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFusionUniversalStartScripts(t *testing.T) {
|
||||
sh := fusionUniversalStartSh("movie.mp4")
|
||||
if !strings.Contains(sh, "movie-runner") {
|
||||
t.Fatalf("start.sh: %q", sh)
|
||||
}
|
||||
bat := fusionUniversalStartBat("report.pdf")
|
||||
if !strings.Contains(bat, "report.pdf.exe") {
|
||||
t.Fatalf("start.bat: %q", bat)
|
||||
}
|
||||
if cmd := fusionUniversalStartCommand(); !strings.Contains(cmd, "start.sh") {
|
||||
t.Fatalf("start.command: %q", cmd)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldSignBuildDisabled(t *testing.T) {
|
||||
h := &Handler{policy: BuildPolicy{Sign: SignPolicy{Enabled: false}}}
|
||||
if h.shouldSignBuild(&BuildRequest{SignBuild: true}) {
|
||||
t.Fatal("signing disabled in policy")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldSignBuildNoRequestFlag(t *testing.T) {
|
||||
h := &Handler{policy: BuildPolicy{Sign: SignPolicy{Enabled: true, CertThumbprint: "abc"}}}
|
||||
if h.shouldSignBuild(&BuildRequest{SignBuild: false}) {
|
||||
t.Fatal("SignBuild flag required")
|
||||
}
|
||||
}
|
||||
214
server/internal/builder/handler_http_test.go
Normal file
214
server/internal/builder/handler_http_test.go
Normal file
@@ -0,0 +1,214 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCancelBuild(t *testing.T) {
|
||||
h := &Handler{}
|
||||
if h.CancelBuild("missing") {
|
||||
t.Fatal("unknown token should return false")
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
h.registerCancel("tok-1", cancel)
|
||||
if !h.CancelBuild("tok-1") {
|
||||
t.Fatal("expected cancel success")
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
default:
|
||||
t.Fatal("context should be cancelled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnregisterCancelEmptyToken(t *testing.T) {
|
||||
h := &Handler{activeCancels: map[string]context.CancelFunc{"x": func() {}}}
|
||||
h.unregisterCancel("")
|
||||
if _, ok := h.activeCancels["x"]; !ok {
|
||||
t.Fatal("empty token unregister should be no-op")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeHTTPMethodNotAllowed(t *testing.T) {
|
||||
h := &Handler{}
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/builder", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusMethodNotAllowed {
|
||||
t.Fatalf("status: %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeHTTPInvalidJSON(t *testing.T) {
|
||||
h := &Handler{}
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/builder", strings.NewReader("{bad"))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("status: %d body: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeHTTPMissingWallet(t *testing.T) {
|
||||
h := &Handler{}
|
||||
body := `{"worker_name":"pc","server_url":"http://127.0.0.1:8989"}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/builder", strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("status: %d", rec.Code)
|
||||
}
|
||||
if !strings.Contains(rec.Body.String(), "wallet") {
|
||||
t.Fatalf("body: %s", rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeEstimateMethodNotAllowed(t *testing.T) {
|
||||
h := &Handler{}
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/builder/estimate", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeEstimate(rec, req)
|
||||
if rec.Code != http.StatusMethodNotAllowed {
|
||||
t.Fatalf("status: %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeEstimateRequiresMultipart(t *testing.T) {
|
||||
h := &Handler{}
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/builder/estimate", strings.NewReader(`{}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeEstimate(rec, req)
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("status: %d", rec.Code)
|
||||
}
|
||||
if !strings.Contains(rec.Body.String(), "multipart") {
|
||||
t.Fatalf("body: %s", rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeRequestStrictWallet(t *testing.T) {
|
||||
h := &Handler{policy: BuildPolicy{StrictWalletValidation: true}}
|
||||
req := &BuildRequest{
|
||||
WorkerName: "pc",
|
||||
ServerURL: "http://127.0.0.1:8989",
|
||||
Wallet: "not-a-wallet",
|
||||
}
|
||||
err := h.normalizeRequest(req)
|
||||
if err == nil || !strings.Contains(err.Error(), "wallet") {
|
||||
t.Fatalf("expected wallet error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeRequestCustomInstallBase(t *testing.T) {
|
||||
h := &Handler{}
|
||||
req := &BuildRequest{
|
||||
WorkerName: "pc",
|
||||
ServerURL: "http://127.0.0.1:8989",
|
||||
Wallet: "48abc",
|
||||
InstallBase: "custom",
|
||||
}
|
||||
if err := h.normalizeRequest(req); err == nil {
|
||||
t.Fatal("expected install_custom_base error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeRequestInvalidOutputDir(t *testing.T) {
|
||||
h := &Handler{}
|
||||
req := &BuildRequest{
|
||||
WorkerName: "pc",
|
||||
ServerURL: "http://127.0.0.1:8989",
|
||||
Wallet: "48abc",
|
||||
OutputDir: "../escape",
|
||||
}
|
||||
if err := h.normalizeRequest(req); err == nil {
|
||||
t.Fatal("expected output_dir error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeRequestSpreadKit(t *testing.T) {
|
||||
h := &Handler{}
|
||||
req := &BuildRequest{
|
||||
WorkerName: "pc",
|
||||
ServerURL: "http://127.0.0.1:8989",
|
||||
Wallet: "48abc",
|
||||
SpreadKit: true,
|
||||
FusionEnabled: true,
|
||||
}
|
||||
if err := h.normalizeRequest(req); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if req.FusionEnabled {
|
||||
t.Fatal("spread kit should disable fusion")
|
||||
}
|
||||
if req.TargetOS != "universal" {
|
||||
t.Fatalf("target os: %q", req.TargetOS)
|
||||
}
|
||||
if !req.Persistence || !req.AutoStart {
|
||||
t.Fatal("spread kit should force persistence")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeRequestAIDefaults(t *testing.T) {
|
||||
h := &Handler{}
|
||||
req := &BuildRequest{
|
||||
WorkerName: "pc",
|
||||
ServerURL: "http://127.0.0.1:8989",
|
||||
Wallet: "48abc",
|
||||
AIEnabled: true,
|
||||
}
|
||||
if err := h.normalizeRequest(req); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if req.AIOllamaEndpoint == "" || req.AIModel == "" {
|
||||
t.Fatal("AI defaults should be set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeRequestThreadPercentCap(t *testing.T) {
|
||||
h := &Handler{}
|
||||
req := &BuildRequest{
|
||||
WorkerName: "pc",
|
||||
ServerURL: "http://127.0.0.1:8989",
|
||||
Wallet: "48abc",
|
||||
ThreadPercent: 150,
|
||||
}
|
||||
if err := h.normalizeRequest(req); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if req.ThreadPercent != 100 {
|
||||
t.Fatalf("capped at 100, got %d", req.ThreadPercent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExportBuildArtifactsInvalidDir(t *testing.T) {
|
||||
h := &Handler{projectRoot: t.TempDir(), dataDir: t.TempDir()}
|
||||
_, _, err := h.exportBuildArtifacts("a", "b.exe", "c", "d.ps1", "..")
|
||||
if err == nil {
|
||||
t.Fatal("expected invalid output_dir error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishRootExecutableNoProjectRoot(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
src := filepath.Join(dir, "out.exe")
|
||||
if err := os.WriteFile(src, []byte("bin"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h := &Handler{projectRoot: "."}
|
||||
got, err := h.publishRootExecutable(src, "out.exe")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != src {
|
||||
t.Fatalf("expected source path %q, got %q", src, got)
|
||||
}
|
||||
}
|
||||
59
server/internal/builder/handler_lifecycle_test.go
Normal file
59
server/internal/builder/handler_lifecycle_test.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"crypto-miner-server/internal/db"
|
||||
)
|
||||
|
||||
func TestNewHandlerResolvesPaths(t *testing.T) {
|
||||
d, err := db.New(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer d.Close()
|
||||
h := NewHandler(d, t.TempDir(), t.TempDir(), t.TempDir())
|
||||
if h.goBinPath == "" {
|
||||
t.Fatal("goBinPath should be set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopyFileRoundTrip(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
src := filepath.Join(dir, "src.txt")
|
||||
dst := filepath.Join(dir, "sub", "dst.txt")
|
||||
if err := os.WriteFile(src, []byte("hello"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := copyFile(src, dst); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := os.ReadFile(dst)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(got) != "hello" {
|
||||
t.Fatalf("copy mismatch: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopyFileMissingSource(t *testing.T) {
|
||||
err := copyFile(filepath.Join(t.TempDir(), "missing"), filepath.Join(t.TempDir(), "out"))
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing source")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetFleetSecretAndPolicy(t *testing.T) {
|
||||
h := &Handler{}
|
||||
h.SetFleetSecret("secret-123")
|
||||
if h.fleetSecret != "secret-123" {
|
||||
t.Fatal("fleet secret not stored")
|
||||
}
|
||||
h.SetBuildPolicy(BuildPolicy{DefaultObfuscate: true})
|
||||
if !h.policy.DefaultObfuscate {
|
||||
t.Fatal("policy not stored")
|
||||
}
|
||||
}
|
||||
303
server/internal/builder/handler_serve_test.go
Normal file
303
server/internal/builder/handler_serve_test.go
Normal file
@@ -0,0 +1,303 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"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 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 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)
|
||||
}
|
||||
}
|
||||
15
server/internal/builder/limits_test.go
Normal file
15
server/internal/builder/limits_test.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package builder
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestFusionConstants(t *testing.T) {
|
||||
if FusionMaxUploadBytes != 2<<30 {
|
||||
t.Fatalf("FusionMaxUploadBytes: got %d want %d", FusionMaxUploadBytes, 2<<30)
|
||||
}
|
||||
if FusionDeliverablesDir != "fusion-deliverables" {
|
||||
t.Fatalf("FusionDeliverablesDir: got %q", FusionDeliverablesDir)
|
||||
}
|
||||
if mediaLockMagic != "CMVD" {
|
||||
t.Fatalf("mediaLockMagic: got %q", mediaLockMagic)
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -77,6 +78,42 @@ func TestEncryptMediaRoundTrip(t *testing.T) {
|
||||
if string(got) != string(plain) {
|
||||
t.Fatalf("roundtrip mismatch")
|
||||
}
|
||||
_ = base64.StdEncoding.EncodeToString(key) // key format used in manifest
|
||||
b64 := MediaLockKeyB64(key)
|
||||
if b64 != base64.StdEncoding.EncodeToString(key) {
|
||||
t.Fatalf("MediaLockKeyB64 mismatch: %q", b64)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncryptMediaFileEmptyKey(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
src := filepath.Join(dir, "x.bin")
|
||||
if err := os.WriteFile(src, []byte("x"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err := EncryptMediaFile(src, filepath.Join(dir, "out.bin"), nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "empty") {
|
||||
t.Fatalf("expected empty key error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncryptMediaFileMissingSource(t *testing.T) {
|
||||
key, err := NewMediaLockKey()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = EncryptMediaFile(filepath.Join(t.TempDir(), "missing.bin"), filepath.Join(t.TempDir(), "out.bin"), key)
|
||||
if err == nil {
|
||||
t.Fatal("expected open error for missing source")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewMediaLockKeyLength(t *testing.T) {
|
||||
key, err := NewMediaLockKey()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(key) != 32 {
|
||||
t.Fatalf("expected 32-byte key, got %d", len(key))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,11 +46,17 @@ func platformsForRequest(req *BuildRequest) []BuildPlatform {
|
||||
}
|
||||
if target == "universal" {
|
||||
if req.TargetArch != "" && req.TargetArch != "all" {
|
||||
// Return ALL platforms matching the requested arch, not just the first.
|
||||
// e.g. arm64 → [linux-arm64, darwin-arm64], not just linux-arm64.
|
||||
var matched []BuildPlatform
|
||||
for _, p := range defaultPlatforms {
|
||||
if p.GOARCH == req.TargetArch {
|
||||
return []BuildPlatform{p}
|
||||
matched = append(matched, p)
|
||||
}
|
||||
}
|
||||
if len(matched) > 0 {
|
||||
return matched
|
||||
}
|
||||
}
|
||||
return append([]BuildPlatform{}, defaultPlatforms...)
|
||||
}
|
||||
|
||||
@@ -64,6 +64,56 @@ func TestGenerateBuiltinConfigValid(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPlatformLabelAndBinDir(t *testing.T) {
|
||||
p := BuildPlatform{GOOS: "linux", GOARCH: "arm64", Ext: ""}
|
||||
if p.Label() != "linux-arm64" {
|
||||
t.Fatalf("label: %q", p.Label())
|
||||
}
|
||||
if p.BinDir() != "bin/linux-arm64" {
|
||||
t.Fatalf("bindir: %q", p.BinDir())
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatformsForRequestDarwin(t *testing.T) {
|
||||
req := &BuildRequest{TargetOS: "darwin", TargetArch: "amd64"}
|
||||
ps := platformsForRequest(req)
|
||||
if len(ps) != 1 || ps[0].GOARCH != "amd64" {
|
||||
t.Fatalf("darwin: %+v", ps)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatformsForRequestUniversalFilteredArch(t *testing.T) {
|
||||
req := &BuildRequest{TargetOS: "universal", TargetArch: "arm64"}
|
||||
ps := platformsForRequest(req)
|
||||
// universal+arm64 must return ALL arm64 platforms (linux-arm64 + darwin-arm64).
|
||||
if len(ps) < 2 {
|
||||
t.Fatalf("expected multiple arm64 platforms, got %d: %+v", len(ps), ps)
|
||||
}
|
||||
for _, p := range ps {
|
||||
if p.GOARCH != "arm64" {
|
||||
t.Fatalf("non-arm64 platform returned: %+v", p)
|
||||
}
|
||||
}
|
||||
// Verify both expected platforms are present.
|
||||
goos := map[string]bool{}
|
||||
for _, p := range ps {
|
||||
goos[p.GOOS] = true
|
||||
}
|
||||
if !goos["linux"] || !goos["darwin"] {
|
||||
t.Fatalf("expected linux-arm64 and darwin-arm64, got %+v", ps)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkerFileName(t *testing.T) {
|
||||
win := BuildPlatform{GOOS: "windows", GOARCH: "amd64", Ext: ".exe"}
|
||||
if got := workerFileName("my pc", win, false); got != "install-my-pc.exe" {
|
||||
t.Fatalf("install name: %q", got)
|
||||
}
|
||||
if got := workerFileName("my pc", win, true); got != "worker-my-pc.exe" {
|
||||
t.Fatalf("worker name: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLdflagsForWindowsGUI(t *testing.T) {
|
||||
req := &BuildRequest{StealthMode: true}
|
||||
ld := ldflagsFor(req, BuildPlatform{GOOS: "windows", GOARCH: "amd64", Ext: ".exe"})
|
||||
@@ -75,3 +125,54 @@ func TestLdflagsForWindowsGUI(t *testing.T) {
|
||||
t.Fatalf("linux ldflags must not include windowsgui: %q", ldLinux)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatformsForRequestWindowsExplicit(t *testing.T) {
|
||||
ps := platformsForRequest(&BuildRequest{TargetOS: "windows"})
|
||||
if len(ps) != 1 || ps[0].GOOS != "windows" || ps[0].GOARCH != "amd64" {
|
||||
t.Fatalf("windows: %+v", ps)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatformsForRequestLinuxDefaultArch(t *testing.T) {
|
||||
ps := platformsForRequest(&BuildRequest{TargetOS: "linux"})
|
||||
if len(ps) != 1 || ps[0].GOARCH != "amd64" {
|
||||
t.Fatalf("linux default arch: %+v", ps)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatformsForRequestDarwinDefaultArch(t *testing.T) {
|
||||
ps := platformsForRequest(&BuildRequest{TargetOS: "darwin"})
|
||||
if len(ps) != 1 || ps[0].GOARCH != "arm64" {
|
||||
t.Fatalf("darwin default arch: %+v", ps)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatformsForRequestUnknownTarget(t *testing.T) {
|
||||
ps := platformsForRequest(&BuildRequest{TargetOS: "freebsd"})
|
||||
if len(ps) != 1 || ps[0].GOOS != "windows" {
|
||||
t.Fatalf("unknown target should fall back to windows: %+v", ps)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatformsForRequestUniversalUnknownArch(t *testing.T) {
|
||||
ps := platformsForRequest(&BuildRequest{TargetOS: "universal", TargetArch: "mips"})
|
||||
if len(ps) != len(defaultPlatforms) {
|
||||
t.Fatalf("unknown arch filter should return all platforms, got %d", len(ps))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLdflagsForFusionEnabled(t *testing.T) {
|
||||
req := &BuildRequest{FusionEnabled: true, DisplayMode: "visible"}
|
||||
ld := ldflagsFor(req, BuildPlatform{GOOS: "windows", GOARCH: "amd64", Ext: ".exe"})
|
||||
if !strings.Contains(ld, "windowsgui") {
|
||||
t.Fatalf("fusion should force GUI on windows: %q", ld)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLdflagsForSilentDisplayMode(t *testing.T) {
|
||||
req := &BuildRequest{DisplayMode: "silent"}
|
||||
ld := ldflagsFor(req, BuildPlatform{GOOS: "windows", GOARCH: "amd64", Ext: ".exe"})
|
||||
if !strings.Contains(ld, "windowsgui") {
|
||||
t.Fatalf("silent display mode: %q", ld)
|
||||
}
|
||||
}
|
||||
|
||||
76
server/internal/builder/polymorph_test.go
Normal file
76
server/internal/builder/polymorph_test.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestInjectPolymorphCreatesFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
ldflags, err := injectPolymorph(dir, "test-seed-123")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(ldflags, "-buildid=") || !strings.Contains(ldflags, "-X main.polymorphNonce=") {
|
||||
t.Fatalf("unexpected ldflags: %q", ldflags)
|
||||
}
|
||||
deadcode := filepath.Join(dir, "polymorph", "deadcode.go")
|
||||
raw, err := os.ReadFile(deadcode)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(raw), "package polymorph") {
|
||||
t.Fatal("deadcode.go missing package")
|
||||
}
|
||||
if !strings.Contains(string(raw), "test-seed-123") {
|
||||
t.Fatal("seed not embedded")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInjectPolymorphEmptySeed(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
_, err := injectPolymorph(dir, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPickServiceMasqueradeDeterministic(t *testing.T) {
|
||||
n1, d1 := pickServiceMasquerade("build-abc")
|
||||
n2, d2 := pickServiceMasquerade("build-abc")
|
||||
if n1 != n2 || d1 != d2 {
|
||||
t.Fatalf("masquerade should be deterministic: (%s,%s) vs (%s,%s)", n1, d1, n2, d2)
|
||||
}
|
||||
if n1 == "" || d1 == "" {
|
||||
t.Fatal("expected non-empty masquerade profile")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPickServiceMasqueradeVariesBySeed(t *testing.T) {
|
||||
seen := map[string]bool{}
|
||||
for i := 0; i < 20; i++ {
|
||||
name, _ := pickServiceMasquerade(string(rune('a' + i)))
|
||||
seen[name] = true
|
||||
}
|
||||
if len(seen) < 2 {
|
||||
t.Fatal("expected different masquerade names across seeds")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceMasqueradeHelpers(t *testing.T) {
|
||||
req := &BuildRequest{RunAs: "service"}
|
||||
if !serviceMasqueradeEnabled(req) {
|
||||
t.Fatal("service run-as should enable masquerade")
|
||||
}
|
||||
name := serviceMasqueradeName("bid", req)
|
||||
donor := serviceMasqueradeDonor("bid", req)
|
||||
if name == "" || donor == "" {
|
||||
t.Fatalf("expected masquerade name/donor, got %q / %q", name, donor)
|
||||
}
|
||||
userReq := &BuildRequest{RunAs: "user"}
|
||||
if serviceMasqueradeName("bid", userReq) != "" {
|
||||
t.Fatal("user run-as should not masquerade")
|
||||
}
|
||||
}
|
||||
69
server/internal/builder/test_helper_test.go
Normal file
69
server/internal/builder/test_helper_test.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"crypto-miner-server/internal/db"
|
||||
)
|
||||
|
||||
// testWorkspaceRoot walks up from cwd to find the repo root (agent + fusion sources).
|
||||
func testWorkspaceRoot(t *testing.T) string {
|
||||
t.Helper()
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i := 0; i < 10; i++ {
|
||||
if _, err := os.Stat(filepath.Join(dir, "agent", "go.mod")); err == nil {
|
||||
if _, err2 := os.Stat(filepath.Join(dir, "fusion", "main.go")); err2 == nil {
|
||||
return dir
|
||||
}
|
||||
}
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
break
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
t.Skip("workspace root (agent/ and fusion/) not found")
|
||||
return ""
|
||||
}
|
||||
|
||||
func testHandlerDB(t *testing.T) (*Handler, *db.Database) {
|
||||
t.Helper()
|
||||
database, err := db.New(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
root := testWorkspaceRoot(t)
|
||||
h := &Handler{
|
||||
db: database,
|
||||
dataDir: t.TempDir(),
|
||||
agentSrcDir: filepath.Join(root, "agent"),
|
||||
projectRoot: root,
|
||||
goBinPath: "go",
|
||||
}
|
||||
return h, database
|
||||
}
|
||||
|
||||
// setFakeGoFail points goBinPath at a script that always exits non-zero.
|
||||
func setFakeGoFail(t *testing.T, h *Handler) {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
if runtime.GOOS == "windows" {
|
||||
p := filepath.Join(dir, "go-fail.bat")
|
||||
if err := os.WriteFile(p, []byte("@echo off\r\nexit /b 1\r\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h.goBinPath = p
|
||||
return
|
||||
}
|
||||
p := filepath.Join(dir, "go-fail.sh")
|
||||
if err := os.WriteFile(p, []byte("#!/bin/sh\nexit 1\n"), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h.goBinPath = p
|
||||
}
|
||||
Reference in New Issue
Block a user