mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-15 16:30:50 +08:00
174 lines
8.2 KiB
Python
174 lines
8.2 KiB
Python
"""Run the production privacy preferences and preview file store on macOS."""
|
|
|
|
from pathlib import Path
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
|
|
|
|
@unittest.skipUnless(sys.platform == "darwin", "requires the Apple Swift runtime")
|
|
class PrivacyControlsTest(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.temp = tempfile.TemporaryDirectory(prefix="crossdesk-privacy-tests-")
|
|
cls.addClassCleanup(cls.temp.cleanup)
|
|
root = Path(cls.temp.name)
|
|
harness = root / "PrivacyTests.swift"
|
|
harness.write_text(r'''
|
|
import Foundation
|
|
|
|
func expect(_ condition: @autoclosure () -> Bool, _ message: String) {
|
|
if !condition() { fatalError(message) }
|
|
}
|
|
|
|
func wait(_ semaphore: DispatchSemaphore) {
|
|
expect(semaphore.wait(timeout: .now() + 10) == .success, "Operation timed out")
|
|
}
|
|
|
|
@main
|
|
enum PrivacyTests {
|
|
static func main() throws {
|
|
let test = CommandLine.arguments[1]
|
|
if test == "consent" || test == "upgrade" {
|
|
let suite = "crossdesk.privacy.tests.\(UUID().uuidString)"
|
|
let defaults = UserDefaults(suiteName: suite)!
|
|
defer { defaults.removePersistentDomain(forName: suite) }
|
|
if test == "upgrade" {
|
|
// Existing identity/history must not be interpreted as consent.
|
|
defaults.set("test-identity", forKey: "crossdesk.mobile.identity")
|
|
defaults.set(Data("legacy history".utf8),
|
|
forKey: "crossdesk.mobile.recent-connections.v1")
|
|
}
|
|
let preferences = PrivacyPreferences(defaults: defaults)
|
|
expect(PrivacyNoticeState(hasNetworkConsent: preferences.hasNetworkConsent).isVisible,
|
|
"New/legacy install must show the policy")
|
|
expect(!preferences.hasNetworkConsent, "Must not start networking")
|
|
expect(!preferences.savesThumbnails, "Saving must default to off")
|
|
preferences.setSavesThumbnails(true)
|
|
expect(!preferences.savesThumbnails, "Cannot enable captures without consent")
|
|
preferences.setNetworkConsent(false)
|
|
let declined = PrivacyPreferences(defaults: defaults)
|
|
expect(!declined.hasNetworkConsent, "Decline must not allow networking")
|
|
expect(PrivacyNoticeState(hasNetworkConsent: declined.hasNetworkConsent).isVisible,
|
|
"Relaunch after refusal must show the policy again")
|
|
declined.setNetworkConsent(true)
|
|
expect(!declined.savesThumbnails, "Network consent is not screenshot consent")
|
|
declined.setSavesThumbnails(true)
|
|
let relaunched = PrivacyPreferences(defaults: defaults)
|
|
expect(relaunched.hasNetworkConsent && relaunched.savesThumbnails,
|
|
"Explicit choices must survive relaunch")
|
|
expect(!PrivacyNoticeState(hasNetworkConsent: relaunched.hasNetworkConsent).isVisible,
|
|
"Relaunch after agreement must not prompt again")
|
|
relaunched.setNetworkConsent(false)
|
|
expect(!relaunched.hasNetworkConsent && !relaunched.savesThumbnails,
|
|
"Withdrawal revokes both permissions")
|
|
expect(PrivacyNoticeState(hasNetworkConsent: relaunched.hasNetworkConsent).isVisible,
|
|
"Relaunch after withdrawal must show the policy")
|
|
relaunched.setNetworkConsent(true)
|
|
expect(!relaunched.savesThumbnails, "Re-consent must not re-enable screenshots")
|
|
} else if test == "notice_lifecycle" {
|
|
var notice = PrivacyNoticeState(hasNetworkConsent: false)
|
|
expect(notice.isVisible, "No consent must prompt on cold launch")
|
|
notice.didBecomeActive(hasNetworkConsent: false)
|
|
notice.dismiss()
|
|
notice.didBecomeActive(hasNetworkConsent: false)
|
|
expect(!notice.isVisible, "Refusal must not immediately reopen the notice")
|
|
notice.didBecomeActive(hasNetworkConsent: false)
|
|
expect(!notice.isVisible, "Transient inactivity or duplicate active callbacks are not another visit")
|
|
for _ in 0..<3 {
|
|
notice.didEnterBackground()
|
|
notice.didBecomeActive(hasNetworkConsent: false)
|
|
expect(notice.isVisible, "Every background/foreground round trip must prompt without consent")
|
|
notice.dismiss()
|
|
}
|
|
notice.show()
|
|
expect(notice.isVisible, "The status button can still open the policy manually")
|
|
notice.dismiss()
|
|
notice.didEnterBackground()
|
|
notice.didBecomeActive(hasNetworkConsent: true)
|
|
expect(!notice.isVisible, "Granting consent stops automatic prompts")
|
|
notice.didEnterBackground()
|
|
notice.didBecomeActive(hasNetworkConsent: false)
|
|
expect(notice.isVisible, "Withdrawal restores the next-visit prompt")
|
|
} else {
|
|
let directory = FileManager.default.temporaryDirectory
|
|
.appendingPathComponent("crossdesk-thumbnails-\(UUID().uuidString)")
|
|
defer { try? FileManager.default.removeItem(at: directory) }
|
|
let store = ThumbnailFileStore(directory: directory)
|
|
let started = DispatchSemaphore(value: 0)
|
|
let finishCapture = DispatchSemaphore(value: 0)
|
|
let cleared = DispatchSemaphore(value: 0)
|
|
store.save(fileName: "remote.jpg", makeData: {
|
|
started.signal()
|
|
wait(finishCapture)
|
|
return Data("sensitive frame".utf8)
|
|
}) { fileName in
|
|
expect(fileName == "remote.jpg", "Capture must exercise the real write path")
|
|
}
|
|
wait(started)
|
|
if test == "clear" {
|
|
// Withdrawal/clear arrives while image conversion is still running.
|
|
store.removeAll { success in
|
|
expect(success, "Clear must succeed")
|
|
cleared.signal()
|
|
}
|
|
} else {
|
|
// Removing a recent connection must also win over its pending capture.
|
|
store.remove(fileName: "remote.jpg")
|
|
store.save(fileName: "barrier.jpg", makeData: { Data([1]) }) { _ in
|
|
cleared.signal()
|
|
}
|
|
}
|
|
finishCapture.signal()
|
|
wait(cleared)
|
|
expect(!FileManager.default.fileExists(atPath: store.url(for: "remote.jpg").path),
|
|
"A pending write must not resurrect a deleted preview")
|
|
// A later explicitly permitted capture can still be saved.
|
|
let saved = DispatchSemaphore(value: 0)
|
|
store.save(fileName: "new.jpg", makeData: { Data("new frame".utf8) }) { name in
|
|
expect(name == "new.jpg", "New captures must work after clearing")
|
|
saved.signal()
|
|
}
|
|
wait(saved)
|
|
let data = try Data(contentsOf: store.url(for: "new.jpg"))
|
|
expect(data == Data("new frame".utf8), "Saved preview must be intact")
|
|
let values = try directory.resourceValues(forKeys: [.isExcludedFromBackupKey])
|
|
expect(values.isExcludedFromBackup == true, "Previews must be excluded from backup")
|
|
}
|
|
}
|
|
}
|
|
''')
|
|
source = Path(__file__).resolve().parents[1] / "CrossDeskMobile"
|
|
cls.binary = root / "privacy-tests"
|
|
subprocess.run([
|
|
"xcrun", "swiftc", "-swift-version", "5",
|
|
str(source / "PrivacyPreferences.swift"),
|
|
str(source / "ThumbnailFileStore.swift"), str(harness),
|
|
"-o", str(cls.binary),
|
|
], check=True, capture_output=True, text=True)
|
|
|
|
def run_case(self, case):
|
|
result = subprocess.run([str(self.binary), case], capture_output=True,
|
|
text=True, timeout=30)
|
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
|
|
def test_consent_persistence_and_withdrawal(self):
|
|
self.run_case("consent")
|
|
|
|
def test_upgrade_requires_fresh_choices(self):
|
|
self.run_case("upgrade")
|
|
|
|
def test_notice_on_each_unconsented_visit(self):
|
|
self.run_case("notice_lifecycle")
|
|
|
|
def test_clear_wins_over_in_flight_capture(self):
|
|
self.run_case("clear")
|
|
|
|
def test_remove_connection_wins_over_in_flight_capture(self):
|
|
self.run_case("remove")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|