659 lines
30 KiB
Swift
659 lines
30 KiB
Swift
import Foundation
|
|
import os.lock
|
|
import Testing
|
|
@testable import CodexBarCore
|
|
|
|
#if os(macOS)
|
|
import SweetCookieKit
|
|
|
|
@Suite(.serialized)
|
|
struct BrowserDetectionTests {
|
|
private func detection(
|
|
homeDirectory: String,
|
|
installedBrowsers: Set<Browser>) -> BrowserDetection
|
|
{
|
|
let installedAppPaths = Set(installedBrowsers.map { "/Applications/\($0.appBundleName).app" })
|
|
return BrowserDetection(
|
|
homeDirectory: homeDirectory,
|
|
cacheTTL: 0,
|
|
now: Date.init,
|
|
fileExists: { path in
|
|
if path.hasSuffix(".app") {
|
|
return installedAppPaths.contains(path)
|
|
}
|
|
return FileManager.default.fileExists(atPath: path)
|
|
},
|
|
directoryContents: { path in
|
|
try? FileManager.default.contentsOfDirectory(atPath: path)
|
|
},
|
|
applicationURLs: { _ in [] },
|
|
profileAccessIssue: { _ in nil })
|
|
}
|
|
|
|
private static func labelIDs(for browser: Browser) -> [String] {
|
|
browser.safeStorageLabels.map { self.labelID(service: $0.service, account: $0.account) }
|
|
}
|
|
|
|
private static func labelID(service: String, account: String?) -> String {
|
|
"\(service)|\(account ?? "")"
|
|
}
|
|
|
|
@Test(.disabled(
|
|
if: ProcessInfo.processInfo.environment[BrowserCookieAccessGate.allowTestCookieAccessEnvironmentKey] == "1",
|
|
"Default-home cookie access is explicitly enabled for this test run."))
|
|
func `default home detection is suppressed before profile probes`() throws {
|
|
let probeCount = OSAllocatedUnfairLock(initialState: 0)
|
|
let defaultHome = try #require(BrowserCookieClient.defaultHomeDirectories().first)
|
|
let detection = BrowserDetection(
|
|
homeDirectory: defaultHome.path,
|
|
cacheTTL: 0,
|
|
fileExists: { _ in
|
|
probeCount.withLock { $0 += 1 }
|
|
return false
|
|
},
|
|
directoryContents: { _ in
|
|
probeCount.withLock { $0 += 1 }
|
|
return nil
|
|
})
|
|
|
|
_ = detection.isCookieSourceAvailable(.chrome)
|
|
#expect(probeCount.withLock { $0 } == 0)
|
|
}
|
|
|
|
@Test(.disabled(
|
|
if: ProcessInfo.processInfo.environment[BrowserCookieAccessGate.allowTestCookieAccessEnvironmentKey] == "1",
|
|
"Default-home cookie access is explicitly enabled for this test run."))
|
|
func `default client reports structured suppression before store discovery`() {
|
|
let client = BrowserCookieClient()
|
|
|
|
#expect(throws: BrowserCookieStoreAccessSuppressedError.self) {
|
|
_ = try client.codexBarStores(for: .chrome)
|
|
}
|
|
#expect(throws: BrowserCookieStoreAccessSuppressedError.self) {
|
|
_ = try client.codexBarRecords(
|
|
matching: BrowserCookieQuery(domains: ["example.com"]),
|
|
in: .safari)
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `cookie store decision allows production and explicit test opt in`() {
|
|
let defaultHomes = BrowserCookieClient.defaultHomeDirectories()
|
|
let testProcess = "swiftpm-testing-helper"
|
|
|
|
#expect(BrowserCookieAccessGate.cookieStoreAccessDecision(
|
|
homeDirectories: defaultHomes,
|
|
processName: testProcess,
|
|
environment: [:]) == .suppressed)
|
|
#expect(BrowserCookieAccessGate.cookieStoreAccessDecision(
|
|
homeDirectories: defaultHomes,
|
|
processName: testProcess,
|
|
environment: [BrowserCookieAccessGate.allowTestCookieAccessEnvironmentKey: "1"]) == .allowed)
|
|
#expect(BrowserCookieAccessGate.cookieStoreAccessDecision(
|
|
homeDirectories: defaultHomes,
|
|
processName: "CodexBar",
|
|
environment: [:]) == .allowed)
|
|
}
|
|
|
|
@Test(.disabled(
|
|
if: ProcessInfo.processInfo.environment[BrowserCookieAccessGate.allowTestCookieAccessEnvironmentKey] == "1",
|
|
"Default-home cookie access is explicitly enabled for this test run."))
|
|
func `safari is installed but default cookie access is disabled during tests`() {
|
|
#expect(BrowserDetection(cacheTTL: 0).isAppInstalled(.safari) == true)
|
|
#expect(BrowserDetection(cacheTTL: 0).isCookieSourceAvailable(.safari) == false)
|
|
}
|
|
|
|
@Test(.disabled(
|
|
if: ProcessInfo.processInfo.environment[BrowserCookieAccessGate.allowTestCookieAccessEnvironmentKey] == "1",
|
|
"Default-home cookie access is explicitly enabled for this test run."))
|
|
func `default cookie candidates exclude safari during tests`() {
|
|
let detection = BrowserDetection(cacheTTL: 0)
|
|
let browsers: [Browser] = [.safari, .chrome, .firefox]
|
|
#expect(browsers.cookieImportCandidates(using: detection).contains(.safari) == false)
|
|
}
|
|
|
|
@Test
|
|
func `explicit isolated home keeps safari cookie source available`() {
|
|
let detection = BrowserDetection(homeDirectory: "/tmp/codexbar-browser-detection", cacheTTL: 0)
|
|
#expect(detection.isCookieSourceAvailable(.safari))
|
|
}
|
|
|
|
@Test
|
|
func `cookie client permits isolated chromium stores during tests`() throws {
|
|
let temp = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
let profile = temp
|
|
.appendingPathComponent("Library/Application Support/Google/Chrome/Default/Network")
|
|
try FileManager.default.createDirectory(at: profile, withIntermediateDirectories: true)
|
|
FileManager.default.createFile(atPath: profile.appendingPathComponent("Cookies").path, contents: Data())
|
|
defer { try? FileManager.default.removeItem(at: temp) }
|
|
|
|
let client = BrowserCookieClient(configuration: .init(homeDirectories: [temp]))
|
|
let stores = try KeychainAccessGate.withTaskOverrideForTesting(false) {
|
|
try KeychainAccessPreflight.withCheckGenericPasswordOverrideForTesting { _, _ in .allowed } operation: {
|
|
try client.codexBarStores(for: .chrome)
|
|
}
|
|
}
|
|
#expect(stores.count == 1)
|
|
}
|
|
|
|
@Test
|
|
func `filter preserves order`() {
|
|
BrowserCookieAccessGate.resetForTesting()
|
|
|
|
let temp = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
try? FileManager.default.createDirectory(at: temp, withIntermediateDirectories: true)
|
|
defer { try? FileManager.default.removeItem(at: temp) }
|
|
|
|
let firefoxProfile = temp
|
|
.appendingPathComponent("Library")
|
|
.appendingPathComponent("Application Support")
|
|
.appendingPathComponent("Firefox")
|
|
.appendingPathComponent("Profiles")
|
|
.appendingPathComponent("abc.default-release")
|
|
try? FileManager.default.createDirectory(at: firefoxProfile, withIntermediateDirectories: true)
|
|
FileManager.default.createFile(
|
|
atPath: firefoxProfile.appendingPathComponent("cookies.sqlite").path,
|
|
contents: Data())
|
|
|
|
let detection = self.detection(homeDirectory: temp.path, installedBrowsers: [.firefox])
|
|
let browsers: [Browser] = [.firefox, .safari, .chrome]
|
|
// Chrome is filtered out deterministically because it lacks usable on-disk profile/cookie store data.
|
|
#expect(browsers.cookieImportCandidates(using: detection) == [.firefox, .safari])
|
|
}
|
|
|
|
@Test
|
|
func `chrome requires profile data`() throws {
|
|
let temp = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
try FileManager.default.createDirectory(at: temp, withIntermediateDirectories: true)
|
|
defer { try? FileManager.default.removeItem(at: temp) }
|
|
|
|
let detection = self.detection(homeDirectory: temp.path, installedBrowsers: [.chrome])
|
|
#expect(detection.isCookieSourceAvailable(.chrome) == false)
|
|
|
|
let profile = temp
|
|
.appendingPathComponent("Library")
|
|
.appendingPathComponent("Application Support")
|
|
.appendingPathComponent("Google")
|
|
.appendingPathComponent("Chrome")
|
|
.appendingPathComponent("Default")
|
|
try FileManager.default.createDirectory(at: profile, withIntermediateDirectories: true)
|
|
let cookiesDir = profile.appendingPathComponent("Network")
|
|
try FileManager.default.createDirectory(at: cookiesDir, withIntermediateDirectories: true)
|
|
FileManager.default.createFile(atPath: cookiesDir.appendingPathComponent("Cookies").path, contents: Data())
|
|
|
|
#expect(detection.isCookieSourceAvailable(.chrome) == true)
|
|
}
|
|
|
|
@Test
|
|
func `process filters chromium candidates despite false global keychain override`() throws {
|
|
guard ProcessInfo.processInfo.environment["CODEXBAR_ALLOW_TEST_KEYCHAIN_ACCESS"] != "1" else { return }
|
|
KeychainAccessGate.resetOverrideForTesting()
|
|
defer { KeychainAccessGate.resetOverrideForTesting() }
|
|
|
|
KeychainAccessGate.isDisabled = false
|
|
|
|
let temp = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
try FileManager.default.createDirectory(at: temp, withIntermediateDirectories: true)
|
|
defer { try? FileManager.default.removeItem(at: temp) }
|
|
|
|
let profile = temp
|
|
.appendingPathComponent("Library")
|
|
.appendingPathComponent("Application Support")
|
|
.appendingPathComponent("Google")
|
|
.appendingPathComponent("Chrome")
|
|
.appendingPathComponent("Default")
|
|
try FileManager.default.createDirectory(at: profile, withIntermediateDirectories: true)
|
|
let cookiesDir = profile.appendingPathComponent("Network")
|
|
try FileManager.default.createDirectory(at: cookiesDir, withIntermediateDirectories: true)
|
|
FileManager.default.createFile(atPath: cookiesDir.appendingPathComponent("Cookies").path, contents: Data())
|
|
|
|
let detection = self.detection(homeDirectory: temp.path, installedBrowsers: [.chrome])
|
|
let browsers: [Browser] = [.chrome, .safari]
|
|
#expect(browsers.cookieImportCandidates(using: detection) == [.safari])
|
|
}
|
|
|
|
@Test
|
|
func `keychain interaction suppresses chromium family during cooldown`() {
|
|
BrowserCookieAccessGate.resetForTesting()
|
|
defer { BrowserCookieAccessGate.resetForTesting() }
|
|
|
|
let start = Date(timeIntervalSince1970: 1000)
|
|
var preflightCount = 0
|
|
|
|
KeychainAccessGate.withTaskOverrideForTesting(false) {
|
|
ProviderInteractionContext.$current.withValue(.background) {
|
|
KeychainAccessPreflight.withCheckGenericPasswordOverrideForTesting { _, _ in
|
|
preflightCount += 1
|
|
return .interactionRequired
|
|
} operation: {
|
|
#expect(BrowserCookieAccessGate.shouldAttempt(.chrome, now: start) == false)
|
|
}
|
|
|
|
KeychainAccessPreflight.withCheckGenericPasswordOverrideForTesting { _, _ in
|
|
preflightCount += 1
|
|
return .allowed
|
|
} operation: {
|
|
#expect(BrowserCookieAccessGate.shouldAttempt(.chrome, now: start.addingTimeInterval(60)) == false)
|
|
#expect(BrowserCookieAccessGate.shouldAttempt(.dia, now: start.addingTimeInterval(60)) == false)
|
|
#expect(
|
|
BrowserCookieAccessGate.shouldAttempt(
|
|
.chrome,
|
|
now: start.addingTimeInterval((60 * 60 * 6) + 1)) == true)
|
|
}
|
|
}
|
|
}
|
|
|
|
#expect(preflightCount == 2)
|
|
}
|
|
|
|
@Test
|
|
func `background cookie import allows authorized chromium keychain sources`() {
|
|
BrowserCookieAccessGate.resetForTesting()
|
|
defer { BrowserCookieAccessGate.resetForTesting() }
|
|
|
|
var preflightCount = 0
|
|
|
|
KeychainAccessGate.withTaskOverrideForTesting(false) {
|
|
KeychainAccessPreflight.withCheckGenericPasswordOverrideForTesting { _, _ in
|
|
preflightCount += 1
|
|
return .allowed
|
|
} operation: {
|
|
ProviderInteractionContext.$current.withValue(.background) {
|
|
#expect(BrowserCookieAccessGate.shouldAttempt(.chrome) == true)
|
|
#expect(BrowserCookieAccessGate.shouldAttempt(.safari) == true)
|
|
}
|
|
}
|
|
}
|
|
|
|
#expect(preflightCount == 1)
|
|
}
|
|
|
|
@Test
|
|
func `background cookie import suppresses chromium keychain sources requiring interaction`() {
|
|
BrowserCookieAccessGate.resetForTesting()
|
|
defer { BrowserCookieAccessGate.resetForTesting() }
|
|
|
|
var preflightCount = 0
|
|
|
|
KeychainAccessGate.withTaskOverrideForTesting(false) {
|
|
KeychainAccessPreflight.withCheckGenericPasswordOverrideForTesting { _, _ in
|
|
preflightCount += 1
|
|
return .interactionRequired
|
|
} operation: {
|
|
ProviderInteractionContext.$current.withValue(.background) {
|
|
#expect(BrowserCookieAccessGate.shouldAttempt(.chrome) == false)
|
|
#expect(BrowserCookieAccessGate.shouldAttempt(.safari) == true)
|
|
}
|
|
}
|
|
}
|
|
|
|
#expect(preflightCount == 1)
|
|
}
|
|
|
|
@Test
|
|
func `recorded browser denial suppresses automatic family and permits explicit source retry`() {
|
|
BrowserCookieAccessGate.resetForTesting()
|
|
defer { BrowserCookieAccessGate.resetForTesting() }
|
|
|
|
let start = Date(timeIntervalSince1970: 1500)
|
|
var preflightCount = 0
|
|
|
|
KeychainAccessGate.withTaskOverrideForTesting(false) {
|
|
BrowserCookieAccessGate.recordIfNeeded(
|
|
BrowserCookieError.accessDenied(browser: .arc, details: "denied"),
|
|
now: start)
|
|
KeychainAccessPreflight.withCheckGenericPasswordOverrideForTesting { _, _ in
|
|
preflightCount += 1
|
|
return .allowed
|
|
} operation: {
|
|
ProviderInteractionContext.$current.withValue(.background) {
|
|
#expect(BrowserCookieAccessGate.shouldAttempt(.chrome, now: start.addingTimeInterval(1)) == false)
|
|
#expect(BrowserCookieAccessGate.shouldAttempt(.edge, now: start.addingTimeInterval(1)) == false)
|
|
#expect(BrowserCookieAccessGate.shouldAttempt(.safari, now: start.addingTimeInterval(1)) == true)
|
|
}
|
|
BrowserCookieAccessGate.withExplicitRetry {
|
|
ProviderInteractionContext.$current.withValue(.userInitiated) {
|
|
#expect(BrowserCookieAccessGate
|
|
.shouldAttempt(.chrome, now: start.addingTimeInterval(2)) == false)
|
|
#expect(BrowserCookieAccessGate.shouldAttempt(.arc, now: start.addingTimeInterval(2)) == true)
|
|
#expect(BrowserCookieAccessGate.claimExplicitRetryCookieReadIfNeeded(for: .arc))
|
|
BrowserCookieAccessGate.recordAllowed(for: .arc)
|
|
}
|
|
}
|
|
ProviderInteractionContext.$current.withValue(.background) {
|
|
#expect(BrowserCookieAccessGate.shouldAttempt(.chrome, now: start.addingTimeInterval(3)) == true)
|
|
}
|
|
}
|
|
}
|
|
|
|
#expect(preflightCount == 2)
|
|
}
|
|
|
|
@Test
|
|
func `denied explicit cookie read closes retry scope`() {
|
|
BrowserCookieAccessGate.resetForTesting()
|
|
defer { BrowserCookieAccessGate.resetForTesting() }
|
|
|
|
let start = Date(timeIntervalSince1970: 1700)
|
|
BrowserCookieAccessGate.recordDenied(for: .arc, now: start)
|
|
|
|
KeychainAccessGate.withTaskOverrideForTesting(false) {
|
|
BrowserCookieAccessGate.withExplicitRetry {
|
|
ProviderInteractionContext.$current.withValue(.userInitiated) {
|
|
#expect(BrowserCookieAccessGate.shouldAttempt(.arc, now: start.addingTimeInterval(1)))
|
|
#expect(BrowserCookieAccessGate.claimExplicitRetryCookieReadIfNeeded(for: .arc))
|
|
|
|
BrowserCookieAccessGate.recordDenied(for: .arc, now: start.addingTimeInterval(2))
|
|
|
|
#expect(BrowserCookieAccessGate.shouldAttempt(.arc, now: start.addingTimeInterval(3)) == false)
|
|
#expect(BrowserCookieAccessGate.shouldAttempt(.edge, now: start.addingTimeInterval(3)) == false)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `chrome keychain preflight queries only chrome labels`() {
|
|
BrowserCookieAccessGate.resetForTesting()
|
|
defer { BrowserCookieAccessGate.resetForTesting() }
|
|
|
|
let chromeLabels = Self.labelIDs(for: .chrome)
|
|
let chromeLabelSet = Set(chromeLabels)
|
|
var queriedLabels: [String] = []
|
|
|
|
KeychainAccessGate.withTaskOverrideForTesting(false) {
|
|
KeychainAccessPreflight.withCheckGenericPasswordOverrideForTesting { service, account in
|
|
let label = Self.labelID(service: service, account: account)
|
|
queriedLabels.append(label)
|
|
return .notFound
|
|
} operation: {
|
|
#expect(BrowserCookieAccessGate.shouldAttempt(.chrome) == true)
|
|
}
|
|
}
|
|
|
|
#expect(queriedLabels == chromeLabels)
|
|
#expect(queriedLabels.allSatisfy { chromeLabelSet.contains($0) })
|
|
}
|
|
|
|
@Test
|
|
func `dia keychain preflight queries only dia labels`() {
|
|
BrowserCookieAccessGate.resetForTesting()
|
|
defer { BrowserCookieAccessGate.resetForTesting() }
|
|
|
|
let diaLabels = Self.labelIDs(for: .dia)
|
|
let diaLabelSet = Set(diaLabels)
|
|
var queriedLabels: [String] = []
|
|
|
|
KeychainAccessGate.withTaskOverrideForTesting(false) {
|
|
KeychainAccessPreflight.withCheckGenericPasswordOverrideForTesting { service, account in
|
|
let label = Self.labelID(service: service, account: account)
|
|
queriedLabels.append(label)
|
|
return .notFound
|
|
} operation: {
|
|
#expect(BrowserCookieAccessGate.shouldAttempt(.dia) == true)
|
|
}
|
|
}
|
|
|
|
#expect(queriedLabels == diaLabels)
|
|
#expect(queriedLabels.allSatisfy { diaLabelSet.contains($0) })
|
|
}
|
|
|
|
@Test
|
|
func `browser keychain interaction suppresses family and permits scoped explicit retry`() throws {
|
|
BrowserCookieAccessGate.resetForTesting()
|
|
defer { BrowserCookieAccessGate.resetForTesting() }
|
|
|
|
let start = Date(timeIntervalSince1970: 2000)
|
|
let chromeLabels = Self.labelIDs(for: .chrome)
|
|
let diaLabels = Self.labelIDs(for: .dia)
|
|
let firstChromeLabel = try #require(chromeLabels.first)
|
|
let firstDiaLabel = try #require(diaLabels.first)
|
|
let allowedLabels = Set(chromeLabels + diaLabels)
|
|
var queriedLabels: [String] = []
|
|
|
|
KeychainAccessGate.withTaskOverrideForTesting(false) {
|
|
KeychainAccessPreflight.withCheckGenericPasswordOverrideForTesting { service, account in
|
|
let label = Self.labelID(service: service, account: account)
|
|
queriedLabels.append(label)
|
|
if label == firstChromeLabel { return .allowed }
|
|
if label == firstDiaLabel { return .interactionRequired }
|
|
return .notFound
|
|
} operation: {
|
|
ProviderInteractionContext.$current.withValue(.background) {
|
|
#expect(BrowserCookieAccessGate.shouldAttempt(.chrome, now: start) == true)
|
|
#expect(BrowserCookieAccessGate.shouldAttempt(.dia, now: start.addingTimeInterval(1)) == false)
|
|
#expect(BrowserCookieAccessGate.shouldAttempt(.chrome, now: start.addingTimeInterval(60)) == false)
|
|
#expect(BrowserCookieAccessGate.shouldAttempt(.dia, now: start.addingTimeInterval(60)) == false)
|
|
}
|
|
BrowserCookieAccessGate.withExplicitRetry {
|
|
ProviderInteractionContext.$current.withValue(.userInitiated) {
|
|
#expect(BrowserCookieAccessGate
|
|
.shouldAttempt(.chrome, now: start.addingTimeInterval(61)) == false)
|
|
#expect(BrowserCookieAccessGate.shouldAttempt(.dia, now: start.addingTimeInterval(61)) == true)
|
|
#expect(BrowserCookieAccessGate
|
|
.shouldAttempt(.edge, now: start.addingTimeInterval(61)) == false)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#expect(queriedLabels == [firstChromeLabel, firstDiaLabel, firstDiaLabel])
|
|
#expect(queriedLabels.allSatisfy { allowedLabels.contains($0) })
|
|
}
|
|
|
|
@Test
|
|
func `dia requires profile data`() throws {
|
|
let temp = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
try FileManager.default.createDirectory(at: temp, withIntermediateDirectories: true)
|
|
defer { try? FileManager.default.removeItem(at: temp) }
|
|
|
|
let detection = self.detection(homeDirectory: temp.path, installedBrowsers: [.dia])
|
|
#expect(detection.isCookieSourceAvailable(.dia) == false)
|
|
|
|
let profile = temp
|
|
.appendingPathComponent("Library")
|
|
.appendingPathComponent("Application Support")
|
|
.appendingPathComponent("Dia")
|
|
.appendingPathComponent("User Data")
|
|
.appendingPathComponent("Default")
|
|
try FileManager.default.createDirectory(at: profile, withIntermediateDirectories: true)
|
|
let cookiesDir = profile.appendingPathComponent("Network")
|
|
try FileManager.default.createDirectory(at: cookiesDir, withIntermediateDirectories: true)
|
|
FileManager.default.createFile(atPath: cookiesDir.appendingPathComponent("Cookies").path, contents: Data())
|
|
|
|
#expect(detection.isCookieSourceAvailable(.dia) == true)
|
|
}
|
|
|
|
@Test
|
|
func `removed browser with stale cookies is not a candidate`() throws {
|
|
let temp = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
let cookies = temp
|
|
.appendingPathComponent("Library/Application Support/Dia/User Data/Default/Network/Cookies")
|
|
try FileManager.default.createDirectory(
|
|
at: cookies.deletingLastPathComponent(),
|
|
withIntermediateDirectories: true)
|
|
FileManager.default.createFile(atPath: cookies.path, contents: Data())
|
|
defer { try? FileManager.default.removeItem(at: temp) }
|
|
|
|
let detection = self.detection(homeDirectory: temp.path, installedBrowsers: [])
|
|
|
|
#expect(detection.hasUsableProfileData(.dia))
|
|
#expect(!detection.isCookieSourceAvailable(.dia))
|
|
#expect([Browser.dia].cookieImportCandidates(using: detection).isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func `browser uninstall invalidates cookie source immediately`() throws {
|
|
let temp = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
let cookies = temp
|
|
.appendingPathComponent("Library/Application Support/Google/Chrome/Default/Network/Cookies")
|
|
try FileManager.default.createDirectory(
|
|
at: cookies.deletingLastPathComponent(),
|
|
withIntermediateDirectories: true)
|
|
FileManager.default.createFile(atPath: cookies.path, contents: Data())
|
|
defer { try? FileManager.default.removeItem(at: temp) }
|
|
|
|
let installed = OSAllocatedUnfairLock(initialState: true)
|
|
let detection = BrowserDetection(
|
|
homeDirectory: temp.path,
|
|
cacheTTL: 600,
|
|
fileExists: { path in
|
|
if path == "/Applications/Google Chrome.app" {
|
|
return installed.withLock { $0 }
|
|
}
|
|
return FileManager.default.fileExists(atPath: path)
|
|
},
|
|
directoryContents: { path in
|
|
try? FileManager.default.contentsOfDirectory(atPath: path)
|
|
})
|
|
|
|
#expect(detection.isCookieSourceAvailable(.chrome))
|
|
installed.withLock { $0 = false }
|
|
#expect(!detection.isCookieSourceAvailable(.chrome))
|
|
}
|
|
|
|
@Test
|
|
func `registered browser outside Applications is a candidate`() throws {
|
|
let temp = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
let cookies = temp
|
|
.appendingPathComponent("Library/Application Support/Google/Chrome/Default/Network/Cookies")
|
|
try FileManager.default.createDirectory(
|
|
at: cookies.deletingLastPathComponent(),
|
|
withIntermediateDirectories: true)
|
|
FileManager.default.createFile(atPath: cookies.path, contents: Data())
|
|
defer { try? FileManager.default.removeItem(at: temp) }
|
|
|
|
let appURL = URL(fileURLWithPath: "/Volumes/Tools/Google Chrome.app")
|
|
let detection = BrowserDetection(
|
|
homeDirectory: temp.path,
|
|
cacheTTL: 0,
|
|
now: Date.init,
|
|
fileExists: { path in
|
|
path == appURL.path || FileManager.default.fileExists(atPath: path)
|
|
},
|
|
directoryContents: { path in
|
|
try? FileManager.default.contentsOfDirectory(atPath: path)
|
|
},
|
|
applicationURLs: { appName in
|
|
appName == Browser.chrome.appBundleName ? [appURL] : []
|
|
},
|
|
profileAccessIssue: { _ in nil })
|
|
|
|
#expect(detection.isCookieSourceAvailable(.chrome))
|
|
}
|
|
|
|
@Test
|
|
func `stale registered browser outside Applications is not a candidate`() throws {
|
|
let temp = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
let cookies = temp
|
|
.appendingPathComponent("Library/Application Support/Google/Chrome/Default/Network/Cookies")
|
|
try FileManager.default.createDirectory(
|
|
at: cookies.deletingLastPathComponent(),
|
|
withIntermediateDirectories: true)
|
|
FileManager.default.createFile(atPath: cookies.path, contents: Data())
|
|
defer { try? FileManager.default.removeItem(at: temp) }
|
|
|
|
let staleAppURL = URL(fileURLWithPath: "/Volumes/Removed/Google Chrome.app")
|
|
let detection = BrowserDetection(
|
|
homeDirectory: temp.path,
|
|
cacheTTL: 0,
|
|
now: Date.init,
|
|
fileExists: { path in
|
|
if path.hasSuffix("/Google Chrome.app") {
|
|
return false
|
|
}
|
|
return FileManager.default.fileExists(atPath: path)
|
|
},
|
|
directoryContents: { path in
|
|
try? FileManager.default.contentsOfDirectory(atPath: path)
|
|
},
|
|
applicationURLs: { appName in
|
|
appName == Browser.chrome.appBundleName ? [staleAppURL] : []
|
|
},
|
|
profileAccessIssue: { _ in nil })
|
|
|
|
#expect(!detection.isCookieSourceAvailable(.chrome))
|
|
}
|
|
|
|
@Test
|
|
func `installed browser reports denied profile access`() {
|
|
let home = "/tmp/codexbar-denied-browser-profile"
|
|
let profileRoot = "\(home)/Library/Application Support/Google/Chrome"
|
|
let detection = BrowserDetection(
|
|
homeDirectory: home,
|
|
cacheTTL: 0,
|
|
now: Date.init,
|
|
fileExists: { path in
|
|
path == "/Applications/Google Chrome.app" || path == profileRoot
|
|
},
|
|
directoryContents: { _ in nil },
|
|
applicationURLs: { _ in [] },
|
|
profileAccessIssue: { _ in .accessDenied })
|
|
|
|
#expect(detection.cookieSourceProfileAccessIssue(.chrome) == .accessDenied)
|
|
#expect(!detection.isCookieSourceAvailable(.chrome))
|
|
}
|
|
|
|
@Test
|
|
func `firefox requires default profile dir`() throws {
|
|
let temp = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
try FileManager.default.createDirectory(at: temp, withIntermediateDirectories: true)
|
|
defer { try? FileManager.default.removeItem(at: temp) }
|
|
|
|
let profiles = temp
|
|
.appendingPathComponent("Library")
|
|
.appendingPathComponent("Application Support")
|
|
.appendingPathComponent("Firefox")
|
|
.appendingPathComponent("Profiles")
|
|
try FileManager.default.createDirectory(at: profiles, withIntermediateDirectories: true)
|
|
|
|
let detection = self.detection(homeDirectory: temp.path, installedBrowsers: [.firefox])
|
|
#expect(detection.isCookieSourceAvailable(.firefox) == false)
|
|
|
|
let profile = profiles.appendingPathComponent("abc.default-release")
|
|
try FileManager.default.createDirectory(at: profile, withIntermediateDirectories: true)
|
|
FileManager.default.createFile(atPath: profile.appendingPathComponent("cookies.sqlite").path, contents: Data())
|
|
#expect(detection.isCookieSourceAvailable(.firefox) == true)
|
|
}
|
|
|
|
@Test
|
|
func `zen accepts uppercase default profile dir`() throws {
|
|
let temp = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
try FileManager.default.createDirectory(at: temp, withIntermediateDirectories: true)
|
|
defer { try? FileManager.default.removeItem(at: temp) }
|
|
|
|
let profiles = temp
|
|
.appendingPathComponent("Library")
|
|
.appendingPathComponent("Application Support")
|
|
.appendingPathComponent("zen")
|
|
.appendingPathComponent("Profiles")
|
|
try FileManager.default.createDirectory(at: profiles, withIntermediateDirectories: true)
|
|
|
|
let detection = self.detection(homeDirectory: temp.path, installedBrowsers: [.zen])
|
|
#expect(detection.isCookieSourceAvailable(.zen) == false)
|
|
|
|
let profile = profiles.appendingPathComponent("abc.Default (release)")
|
|
try FileManager.default.createDirectory(at: profile, withIntermediateDirectories: true)
|
|
FileManager.default.createFile(atPath: profile.appendingPathComponent("cookies.sqlite").path, contents: Data())
|
|
#expect(detection.isCookieSourceAvailable(.zen) == true)
|
|
}
|
|
}
|
|
|
|
#else
|
|
|
|
struct BrowserDetectionTests {
|
|
@Test
|
|
func `non mac OS returns no browsers`() {
|
|
#expect(BrowserDetection(cacheTTL: 0).isCookieSourceAvailable(Browser()) == false)
|
|
}
|
|
|
|
@Test
|
|
func `non mac OS filter returns empty`() {
|
|
let detection = BrowserDetection(cacheTTL: 0)
|
|
let browsers = [Browser(), Browser()]
|
|
#expect(browsers.cookieImportCandidates(using: detection).isEmpty == true)
|
|
}
|
|
}
|
|
|
|
#endif
|