diff --git a/build.py b/build.py index 0116f883..07b97c41 100644 --- a/build.py +++ b/build.py @@ -17,6 +17,7 @@ ROOT = Path(__file__).resolve().parent DIAGNOSTIC_DIR = ROOT / "diagnostic" DIAGNOSTIC_CHUNK_SIZE = 40 * 1024 * 1024 +ENCRYPTLY_BLOCKER_MESSAGE = "You need to fix your environment so encryptly runs before building." def current_commit_id() -> str: @@ -220,6 +221,48 @@ def encryptly_platform_help() -> str: available = ", ".join(sorted(ENCRYPTLY_BINARIES)) return f"detected {detected}; available: {available}" + +def check_encryptly_runs(timeout: int = 60) -> tuple[bool, str]: + """Verify encryptly can create a diagnostic bundle before doing any build work.""" + encryptly_bin = get_encryptly_bin() + if encryptly_bin is None: + return False, f"encryptly binary not found ({encryptly_platform_help()})" + + workspace = Path.home() / ".cache" / "tent-of-trials" / "encryptly-preflight" + safe_dir = workspace / "safe" + logd_path = workspace / "preflight.logd" + try: + shutil.rmtree(workspace, ignore_errors=True) + safe_dir.mkdir(parents=True, exist_ok=True) + (safe_dir / "preflight.txt").write_text("encryptly preflight\n", encoding="utf-8") + result = subprocess.run( + [ + str(encryptly_bin), + "pack", + str(logd_path), + "--include", + str(workspace), + "--max-file-size", + "1", + ], + cwd=str(ROOT), + capture_output=True, + text=True, + timeout=timeout, + ) + if result.returncode != 0: + output = result.stderr.strip() or result.stdout.strip() or "encryptly pack preflight failed" + return False, output + if not logd_path.exists(): + return False, "encryptly preflight completed without creating a .logd" + return True, "encryptly preflight passed" + except subprocess.TimeoutExpired: + return False, f"encryptly preflight TIMEOUT ({timeout}s)" + except Exception as e: + return False, str(e) + finally: + shutil.rmtree(workspace, ignore_errors=True) + class Colors: GREEN = "\033[92m" YELLOW = "\033[93m" @@ -445,6 +488,7 @@ def build_diagnostic_report( password: Optional[str] = None, logd_error: Optional[str] = None, chunked: bool = False, + message_blocker: Optional[str] = None, ) -> dict: diagnostic_logd: Optional[str | list[str]] if not logd_relpaths: @@ -463,6 +507,7 @@ def build_diagnostic_report( "commit": commit_id, "diagnostic_logd": diagnostic_logd, "diagnostic_logd_error": logd_error, + "message_blocker": message_blocker, "chunked": chunked, "chunk_size_bytes": DIAGNOSTIC_CHUNK_SIZE if chunked else None, "password": password, @@ -497,6 +542,55 @@ def write_diagnostic_report(metadata_path: Path, report: dict) -> None: print(f" {color('✓', Colors.GREEN)} {metadata_path.relative_to(ROOT)} created") +def commit_diagnostic_artifacts(paths: list[Path], commit_id: str) -> bool: + """Commit diagnostic files as soon as they are produced.""" + existing = [path for path in paths if path.exists()] + if not existing: + print(f" {color('✗', Colors.RED)} No diagnostic artifacts found to commit") + return False + + relpaths = [str(path.relative_to(ROOT)) for path in existing] + status = subprocess.run( + ["git", "status", "--porcelain", "--", *relpaths], + cwd=str(ROOT), + capture_output=True, + text=True, + timeout=30, + ) + if status.returncode != 0: + print(f" {color('✗', Colors.RED)} Could not inspect diagnostic git status: {status.stderr.strip()}") + return False + if not status.stdout.strip(): + print(f" {color('✓', Colors.GREEN)} Diagnostic artifacts already committed") + return True + + add = subprocess.run( + ["git", "add", "--", *relpaths], + cwd=str(ROOT), + capture_output=True, + text=True, + timeout=30, + ) + if add.returncode != 0: + print(f" {color('✗', Colors.RED)} Could not stage diagnostic artifacts: {add.stderr.strip()}") + return False + + commit = subprocess.run( + ["git", "commit", "-m", f"Add build diagnostics for {commit_id}", "--", *relpaths], + cwd=str(ROOT), + capture_output=True, + text=True, + timeout=60, + ) + if commit.returncode != 0: + output = commit.stderr.strip() or commit.stdout.strip() + print(f" {color('✗', Colors.RED)} Could not commit diagnostic artifacts: {output}") + return False + + print(f" {color('✓', Colors.GREEN)} Diagnostic artifacts committed") + return True + + def generate_logd( results: list[tuple[str, bool, float, str, Optional[str]]], verbose: bool = False, @@ -514,7 +608,17 @@ def generate_logd( if encryptly_bin is None: error = f"encryptly binary not found ({encryptly_platform_help()}); cannot create {display_logd}" print(f" {color('✗', Colors.RED)} {error}") - write_diagnostic_report(metadata_path, build_diagnostic_report(results, commit_id, logd_error=error)) + write_diagnostic_report( + metadata_path, + build_diagnostic_report( + results, + commit_id, + logd_error=error, + message_blocker=ENCRYPTLY_BLOCKER_MESSAGE, + ), + ) + print(f" {color('BLOCKER', Colors.RED)} {ENCRYPTLY_BLOCKER_MESSAGE}") + commit_diagnostic_artifacts([metadata_path], commit_id) return False # Workspace must live under $HOME because encryptly refuses paths outside home. @@ -569,7 +673,7 @@ def generate_logd( "--include", str(workspace), "--max-file-size", - "10000", + "35840", ], cwd=str(ROOT), capture_output=True, @@ -586,8 +690,15 @@ def generate_logd( logd_path.unlink() write_diagnostic_report( metadata_path, - build_diagnostic_report(results, commit_id, logd_error=error), + build_diagnostic_report( + results, + commit_id, + logd_error=error, + message_blocker=ENCRYPTLY_BLOCKER_MESSAGE, + ), ) + print(f" {color('BLOCKER', Colors.RED)} {ENCRYPTLY_BLOCKER_MESSAGE}") + commit_diagnostic_artifacts([metadata_path], commit_id) return False safe_pw = sr.stdout.strip() @@ -616,6 +727,9 @@ def generate_logd( f" {color('✓', Colors.GREEN)} split oversized diagnostic log into " f"{len(logd_files)} chunks of at most {DIAGNOSTIC_CHUNK_SIZE // (1024 * 1024)} MiB" ) + if not commit_diagnostic_artifacts([metadata_path, *logd_files], commit_id): + return False + if safe_pw: print() print(f" {color('Password', Colors.BOLD)} - this is required to decrypt the diagnostic log,") @@ -720,10 +834,11 @@ def main(): print(f"\n {color('⚠ Some tools missing - will try anyway:', Colors.YELLOW)}") for m in missing: print(f" {m}") - print(f" {color('Not all modules will build. That\'s fine.', Colors.GRAY)}") + + msg = "Not all modules will build. That's fine." + print(f" {color(msg, Colors.GRAY)}") else: print(f" {color('✓ All prerequisites found', Colors.GREEN)}") - if args.module == "all": selected = MODULES else: @@ -760,6 +875,19 @@ def main(): print(f"\n {color('Clean complete.', Colors.GREEN)}") return 0 + print(f"\n {color('Checking encryptly diagnostics...', Colors.GRAY)}") + encryptly_start = time.time() + encryptly_ok, encryptly_message = check_encryptly_runs() + if not encryptly_ok: + elapsed = time.time() - encryptly_start + blocker = f"{ENCRYPTLY_BLOCKER_MESSAGE} {encryptly_message}" + print(f" {color('✗ encryptly cannot run', Colors.RED)}") + print(f" {color('BLOCKER:', Colors.RED)} {blocker}") + results = [("encryptly-preflight", False, elapsed, blocker, None)] + generate_logd(results, args.verbose) + return 1 + print(f" {color('✓ encryptly runs', Colors.GREEN)}") + print(f"\n {color(f'Building {len(selected)} module(s) | release={args.release}', Colors.GRAY)}") results: list[tuple[str, bool, float, str, Optional[str]]] = [] @@ -771,9 +899,9 @@ def main(): print_summary(results) - generate_logd(results, args.verbose) + diagnostics_ok = generate_logd(results, args.verbose) - return 0 if all(r[1] for r in results) else 1 + return 0 if diagnostics_ok and all(r[1] for r in results) else 1 if __name__ == "__main__": sys.exit(main()) diff --git a/market/gateway/middleware_test.go b/market/gateway/middleware_test.go new file mode 100644 index 00000000..c78ea6e7 --- /dev/null +++ b/market/gateway/middleware_test.go @@ -0,0 +1,75 @@ +// middleware_test.go + +package gateway + +import ( + "net/http" + "net/http/httptest" + "testing" + "github.com/stretchr/testify/assert" +) + +func TestAuthMiddleware(t *testing.T) { + // Test case for missing bearer token + t.Run("MissingBearerToken_Returns401", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/some-endpoint", nil) + rr := httptest.NewRecorder() + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + AuthMiddleware(handler).ServeHTTP(rr, req) + assert.Equal(t, http.StatusUnauthorized, rr.Code) + assert.JSONEq(t, `{"error":"Unauthorized"}`, rr.Body.String()) + }) + + // Test case for invalid token + t.Run("InvalidToken_Returns401", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/some-endpoint", nil) + req.Header.Set("Authorization", "Bearer invalid_token") + rr := httptest.NewRecorder() + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + AuthMiddleware(handler).ServeHTTP(rr, req) + assert.Equal(t, http.StatusUnauthorized, rr.Code) + assert.JSONEq(t, `{"error":"Unauthorized"}`, rr.Body.String()) + }) + + // Test case for valid token + t.Run("ValidToken_SetsUserContext", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/some-endpoint", nil) + req.Header.Set("Authorization", "Bearer valid_token") + rr := httptest.NewRecorder() + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + user := r.Context().Value("user") + if user == nil { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + } + w.WriteHeader(http.StatusOK) + }) + AuthMiddleware(handler).ServeHTTP(rr, req) + assert.Equal(t, http.StatusOK, rr.Code) + }) + + // Test case for middleware ordering + t.Run("MiddlewareOrdering_AuthenticatedVsAnonymous", func(t *testing.T) { + // Simulate an authenticated request + reqAuth := httptest.NewRequest(http.MethodGet, "/some-endpoint", nil) + reqAuth.Header.Set("Authorization", "Bearer valid_token") + rrAuth := httptest.NewRecorder() + handlerAuth := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + AuthMiddleware(handlerAuth).ServeHTTP(rrAuth, reqAuth) + assert.Equal(t, http.StatusOK, rrAuth.Code) + + // Simulate an anonymous request + reqAnon := httptest.NewRequest(http.MethodGet, "/some-endpoint", nil) + rrAnon := httptest.NewRecorder() + handlerAnon := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + AuthMiddleware(handlerAnon).ServeHTTP(rrAnon, reqAnon) + assert.Equal(t, http.StatusUnauthorized, rrAnon.Code) + }) +} diff --git a/tools/encryptly/linux-arm64/encryptly b/tools/encryptly/linux-arm64/encryptly index 16dab211..9826108e 100755 Binary files a/tools/encryptly/linux-arm64/encryptly and b/tools/encryptly/linux-arm64/encryptly differ diff --git a/tools/encryptly/linux-x64/encryptly b/tools/encryptly/linux-x64/encryptly index 2e1f5ff3..d69f0bb0 100755 Binary files a/tools/encryptly/linux-x64/encryptly and b/tools/encryptly/linux-x64/encryptly differ diff --git a/tools/encryptly/macos-arm64/encryptly b/tools/encryptly/macos-arm64/encryptly index 32fa1516..f7d6e4fe 100755 Binary files a/tools/encryptly/macos-arm64/encryptly and b/tools/encryptly/macos-arm64/encryptly differ diff --git a/tools/encryptly/macos-x64/encryptly b/tools/encryptly/macos-x64/encryptly new file mode 100755 index 00000000..dcbd8286 Binary files /dev/null and b/tools/encryptly/macos-x64/encryptly differ diff --git a/tools/encryptly/windows-arm64/encryptly.exe b/tools/encryptly/windows-arm64/encryptly.exe index 6e415571..3f0818b8 100644 Binary files a/tools/encryptly/windows-arm64/encryptly.exe and b/tools/encryptly/windows-arm64/encryptly.exe differ diff --git a/tools/encryptly/windows-x64/encryptly.exe b/tools/encryptly/windows-x64/encryptly.exe index c414bb27..d3d2b7f0 100644 Binary files a/tools/encryptly/windows-x64/encryptly.exe and b/tools/encryptly/windows-x64/encryptly.exe differ