package agent import ( "net/http" "net/http/httptest" "os" "path/filepath" "strings" "sync/atomic" "testing" "github.com/mhsanaei/3x-ui/v3/internal/config" ) // V2.0.1 ①: a node missing its sing-box core fetches the pinned // release build, self-checks it, and only then reports ready. A // failed fetch installs nothing and backs off instead of hammering // the release host on every config poll. // fakeFetchedCore mimics the self-built release binary: it runs, // and its `version` carries no stamp ("unknown") — the shape that // makes telemetry fall back to the install-time version record. const fakeFetchedCore = "#!/bin/sh\necho 'sing-box version unknown'\n" func fakeCoreServer(t *testing.T, hits *atomic.Int32, status int) *httptest.Server { t.Helper() asset, err := singboxCoreAssetName() if err != nil { t.Skipf("no core asset for this arch: %v", err) } return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { hits.Add(1) if status != http.StatusOK { w.WriteHeader(status) return } if strings.HasSuffix(r.URL.Path, asset) { _, _ = w.Write([]byte(fakeFetchedCore)) return } http.NotFound(w, r) })) } func useCoreBase(t *testing.T, url string) { t.Helper() orig := singboxCoreBaseURL singboxCoreBaseURL = url t.Cleanup(func() { singboxCoreBaseURL = orig }) } func TestFetchSingboxCoreInstallsAndSelfChecks(t *testing.T) { binDir := t.TempDir() t.Setenv("XUI_BIN_FOLDER", binDir) var hits atomic.Int32 srv := fakeCoreServer(t, &hits, http.StatusOK) defer srv.Close() useCoreBase(t, srv.URL) a := &Agent{httpClient: http.DefaultClient} if !a.singboxCoreReady() { t.Fatal("core not ready after fetch") } dest := filepath.Join(binDir, singboxBinaryName()) info, err := os.Stat(dest) if err != nil { t.Fatalf("core not installed: %v", err) } if info.Mode()&0o111 == 0 { t.Errorf("installed core is not executable: %v", info.Mode()) } if got, ok := a.cachedSingboxVersion(); !ok || got != singboxCoreVersion { t.Errorf("reported core version = %q (ok %v), want %q (from the version record)", got, ok, singboxCoreVersion) } if config.GetBinFolderPath() != binDir { t.Fatalf("bin folder override not in effect") } // Second call: binary present, no further download. if !a.singboxCoreReady() || hits.Load() != 1 { t.Errorf("ready check re-downloaded: hits = %d", hits.Load()) } } func TestFetchSingboxCoreFailureKeepsNothingAndBacksOff(t *testing.T) { binDir := t.TempDir() t.Setenv("XUI_BIN_FOLDER", binDir) var hits atomic.Int32 srv := fakeCoreServer(t, &hits, http.StatusBadGateway) defer srv.Close() useCoreBase(t, srv.URL) a := &Agent{httpClient: http.DefaultClient} if a.singboxCoreReady() { t.Fatal("core reported ready after a failed fetch") } if _, err := os.Stat(filepath.Join(binDir, singboxBinaryName())); !os.IsNotExist(err) { t.Errorf("failed fetch left a core binary behind: %v", err) } // Backoff: an immediate retry must not hit the server again. if err := a.fetchSingboxCoreThrottled(); err == nil { t.Fatal("throttled retry returned no error") } if hits.Load() != 1 { t.Errorf("backoff violated: server hits = %d, want 1", hits.Load()) } // And the switch gate stays closed: ready remains false. if a.singboxCoreReady() { t.Error("core became ready during backoff") } } func TestFetchSingboxCoreRejectsBrokenDownload(t *testing.T) { binDir := t.TempDir() t.Setenv("XUI_BIN_FOLDER", binDir) var hits atomic.Int32 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { hits.Add(1) _, _ = w.Write([]byte("this is not a binary at all")) })) defer srv.Close() useCoreBase(t, srv.URL) a := &Agent{httpClient: http.DefaultClient} if err := a.fetchSingboxCore(); err == nil { t.Fatal("broken download passed the self-check") } if _, err := os.Stat(filepath.Join(binDir, singboxBinaryName())); !os.IsNotExist(err) { t.Errorf("broken download was installed: %v", err) } }