import XCTest import Foundation import AppKit #if canImport(cmux_DEV) @testable import cmux_DEV #elseif canImport(cmux) @testable import cmux #endif /// Regression test: ensures UpdatePill is never gated behind #if DEBUG in production code paths. /// This prevents accidentally hiding the update UI in Release builds. final class UpdatePillReleaseVisibilityTests: XCTestCase { /// Source files that must show UpdatePill without #if DEBUG guards. private let filesToCheck = [ "Sources/Update/UpdateTitlebarAccessory.swift", "Sources/ContentView.swift", ] func testUpdatePillNotGatedBehindDebug() throws { let projectRoot = findProjectRoot() for relativePath in filesToCheck { let url = projectRoot.appendingPathComponent(relativePath) let source = try String(contentsOf: url, encoding: .utf8) let lines = source.components(separatedBy: .newlines) // Track #if DEBUG nesting depth. var debugDepth = 0 for (index, line) in lines.enumerated() { let trimmed = line.trimmingCharacters(in: .whitespaces) if trimmed == "#if DEBUG" || trimmed.hasPrefix("#if DEBUG ") { debugDepth += 1 } else if trimmed == "#endif" && debugDepth > 0 { debugDepth -= 1 } else if trimmed == "#else" && debugDepth > 0 { // #else inside #if DEBUG means we're in the non-debug branch — that's fine. // But UpdatePill in the #if DEBUG branch (before #else) is the problem. // We handle this by only flagging UpdatePill when debugDepth > 0 and we haven't // hit #else yet. For simplicity, treat #else as flipping out of the guarded section. debugDepth -= 1 } if debugDepth > 0 && trimmed.contains("UpdatePill") { XCTFail( """ \(relativePath):\(index + 1) — UpdatePill is inside #if DEBUG. \ This hides the update UI in Release builds. Remove the #if DEBUG guard \ or move UpdatePill to the #else branch. """ ) } } } } private func findProjectRoot() -> URL { // Walk up from the test bundle to find the project root (contains GhosttyTabs.xcodeproj). var dir = URL(fileURLWithPath: #file).deletingLastPathComponent().deletingLastPathComponent() for _ in 0..<10 { let marker = dir.appendingPathComponent("GhosttyTabs.xcodeproj") if FileManager.default.fileExists(atPath: marker.path) { return dir } dir = dir.deletingLastPathComponent() } // Fallback: assume CWD is project root. return URL(fileURLWithPath: FileManager.default.currentDirectoryPath) } } /// Regression test: ensure WKWebView can load HTTP development URLs (e.g. *.localtest.me). final class AppTransportSecurityTests: XCTestCase { func testInfoPlistAllowsArbitraryLoadsInWebContent() throws { let projectRoot = findProjectRoot() let infoPlistURL = projectRoot.appendingPathComponent("Resources/Info.plist") let data = try Data(contentsOf: infoPlistURL) var format = PropertyListSerialization.PropertyListFormat.xml let plist = try XCTUnwrap( PropertyListSerialization.propertyList(from: data, options: [], format: &format) as? [String: Any] ) let ats = try XCTUnwrap(plist["NSAppTransportSecurity"] as? [String: Any]) XCTAssertEqual( ats["NSAllowsArbitraryLoadsInWebContent"] as? Bool, true, "Resources/Info.plist must allow HTTP loads in WKWebView for local dev hostnames." ) } private func findProjectRoot() -> URL { var dir = URL(fileURLWithPath: #file).deletingLastPathComponent().deletingLastPathComponent() for _ in 0..<10 { let marker = dir.appendingPathComponent("GhosttyTabs.xcodeproj") if FileManager.default.fileExists(atPath: marker.path) { return dir } dir = dir.deletingLastPathComponent() } return URL(fileURLWithPath: FileManager.default.currentDirectoryPath) } } final class BrowserInsecureHTTPSettingsTests: XCTestCase { func testDefaultAllowlistPatternsArePresent() { XCTAssertEqual( BrowserInsecureHTTPSettings.normalizedAllowlistPatterns(rawValue: nil), ["localhost", "127.0.0.1", "::1", "0.0.0.0", "*.localtest.me"] ) } func testWildcardAndExactHostMatching() { XCTAssertTrue(BrowserInsecureHTTPSettings.isHostAllowed("localhost", rawAllowlist: nil)) XCTAssertTrue(BrowserInsecureHTTPSettings.isHostAllowed("127.0.0.1", rawAllowlist: nil)) XCTAssertTrue(BrowserInsecureHTTPSettings.isHostAllowed("::1", rawAllowlist: nil)) XCTAssertTrue(BrowserInsecureHTTPSettings.isHostAllowed("0.0.0.0", rawAllowlist: nil)) XCTAssertTrue(BrowserInsecureHTTPSettings.isHostAllowed("api.localtest.me", rawAllowlist: nil)) XCTAssertFalse(BrowserInsecureHTTPSettings.isHostAllowed("neverssl.com", rawAllowlist: nil)) } func testCustomAllowlistNormalizesAndDeduplicatesEntries() { let raw = """ localhost *.example.com 127.0.0.1 https://dev.internal:8080/path *.example.com """ XCTAssertEqual( BrowserInsecureHTTPSettings.normalizedAllowlistPatterns(rawValue: raw), ["localhost", "*.example.com", "127.0.0.1", "dev.internal"] ) XCTAssertTrue(BrowserInsecureHTTPSettings.isHostAllowed("foo.example.com", rawAllowlist: raw)) XCTAssertTrue(BrowserInsecureHTTPSettings.isHostAllowed("dev.internal", rawAllowlist: raw)) XCTAssertFalse(BrowserInsecureHTTPSettings.isHostAllowed("example.net", rawAllowlist: raw)) } func testBlockDecisionUsesAllowlistAndSchemeRules() throws { let localURL = try XCTUnwrap(URL(string: "http://foo.localtest.me:3000")) XCTAssertFalse(browserShouldBlockInsecureHTTPURL(localURL, rawAllowlist: nil)) let insecureURL = try XCTUnwrap(URL(string: "http://neverssl.com")) XCTAssertTrue(browserShouldBlockInsecureHTTPURL(insecureURL, rawAllowlist: nil)) let httpsURL = try XCTUnwrap(URL(string: "https://neverssl.com")) XCTAssertFalse(browserShouldBlockInsecureHTTPURL(httpsURL, rawAllowlist: nil)) } func testPreparedNavigationRequestPreservesOriginalMethodBodyAndHeaders() throws { let url = try XCTUnwrap(URL(string: "http://localtest.me:3000/submit")) var request = URLRequest(url: url) request.httpMethod = "POST" request.httpBody = Data("token=abc123".utf8) request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") request.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData let prepared = browserPreparedNavigationRequest(request) XCTAssertEqual(prepared.url, url) XCTAssertEqual(prepared.httpMethod, "POST") XCTAssertEqual(prepared.httpBody, Data("token=abc123".utf8)) XCTAssertEqual(prepared.value(forHTTPHeaderField: "Content-Type"), "application/x-www-form-urlencoded") XCTAssertEqual(prepared.cachePolicy, .useProtocolCachePolicy) } func testOneTimeBypassIsConsumedAfterFirstNavigation() throws { let insecureURL = try XCTUnwrap(URL(string: "http://neverssl.com")) var bypassHostOnce: String? = "neverssl.com" XCTAssertTrue(browserShouldConsumeOneTimeInsecureHTTPBypass( insecureURL, bypassHostOnce: &bypassHostOnce )) XCTAssertNil(bypassHostOnce) // Subsequent visits should prompt again unless host was saved. XCTAssertFalse(browserShouldConsumeOneTimeInsecureHTTPBypass( insecureURL, bypassHostOnce: &bypassHostOnce )) XCTAssertTrue(browserShouldBlockInsecureHTTPURL(insecureURL, rawAllowlist: nil)) } func testAddAllowedHostPersistsToDefaultsAndUnblocksHTTP() throws { let suiteName = "BrowserInsecureHTTPSettingsTests.Persist.\(UUID().uuidString)" guard let defaults = UserDefaults(suiteName: suiteName) else { XCTFail("Failed to create isolated UserDefaults suite") return } defer { defaults.removePersistentDomain(forName: suiteName) } let url = try XCTUnwrap(URL(string: "http://persist-me.test")) XCTAssertTrue(browserShouldBlockInsecureHTTPURL(url, defaults: defaults)) BrowserInsecureHTTPSettings.addAllowedHost("persist-me.test", defaults: defaults) let persisted = defaults.string(forKey: BrowserInsecureHTTPSettings.allowlistKey) XCTAssertNotNil(persisted) XCTAssertTrue(BrowserInsecureHTTPSettings.isHostAllowed("persist-me.test", defaults: defaults)) XCTAssertFalse(browserShouldBlockInsecureHTTPURL(url, defaults: defaults)) } func testAllowlistSelectionPersistsForProceedAndOpenExternal() { XCTAssertTrue(browserShouldPersistInsecureHTTPAllowlistSelection( response: .alertFirstButtonReturn, suppressionEnabled: true )) XCTAssertTrue(browserShouldPersistInsecureHTTPAllowlistSelection( response: .alertSecondButtonReturn, suppressionEnabled: true )) XCTAssertFalse(browserShouldPersistInsecureHTTPAllowlistSelection( response: .alertThirdButtonReturn, suppressionEnabled: true )) XCTAssertFalse(browserShouldPersistInsecureHTTPAllowlistSelection( response: .alertSecondButtonReturn, suppressionEnabled: false )) } } final class TitlebarControlsSizingPolicyTests: XCTestCase { func testSchedulePolicyRequiresMeaningfulViewSizeChange() { XCTAssertFalse(titlebarControlsShouldScheduleForViewSizeChange(previous: .zero, current: .zero)) XCTAssertTrue( titlebarControlsShouldScheduleForViewSizeChange( previous: .zero, current: NSSize(width: 240, height: 38) ) ) XCTAssertFalse( titlebarControlsShouldScheduleForViewSizeChange( previous: NSSize(width: 240, height: 38), current: NSSize(width: 240.2, height: 38.1) ) ) XCTAssertTrue( titlebarControlsShouldScheduleForViewSizeChange( previous: NSSize(width: 240, height: 38), current: NSSize(width: 247, height: 38) ) ) } func testLayoutApplyPolicySkipsEquivalentSnapshots() { let baseline = TitlebarControlsLayoutSnapshot( contentSize: NSSize(width: 128, height: 22), containerHeight: 28, yOffset: 3 ) XCTAssertTrue(titlebarControlsShouldApplyLayout(previous: nil, next: baseline)) XCTAssertFalse(titlebarControlsShouldApplyLayout(previous: baseline, next: baseline)) let changed = TitlebarControlsLayoutSnapshot( contentSize: NSSize(width: 132, height: 22), containerHeight: 28, yOffset: 3 ) XCTAssertTrue(titlebarControlsShouldApplyLayout(previous: baseline, next: changed)) } } /// Regression test: ensure new terminal windows are born in full-size content mode so /// titlebar/content offsets are correct before the first resize. final class MainWindowLayoutStyleTests: XCTestCase { func testCreateMainWindowUsesFullSizeContentViewStyleMask() throws { let projectRoot = findProjectRoot() let appDelegateURL = projectRoot.appendingPathComponent("Sources/AppDelegate.swift") let source = try String(contentsOf: appDelegateURL, encoding: .utf8) guard let start = source.range(of: "func createMainWindow("), let end = source.range(of: "@objc func checkForUpdates", range: start.upperBound.. URL { var dir = URL(fileURLWithPath: #file).deletingLastPathComponent().deletingLastPathComponent() for _ in 0..<10 { let marker = dir.appendingPathComponent("GhosttyTabs.xcodeproj") if FileManager.default.fileExists(atPath: marker.path) { return dir } dir = dir.deletingLastPathComponent() } return URL(fileURLWithPath: FileManager.default.currentDirectoryPath) } }