Files
buzz/mobile/ios/BuzzPushKit/Tests/BuzzPushKitTests/BuzzAgeRestrictionSessionTests.swift
Tom Brow 779af8886c fix(mobile): fail open when age checks are unavailable (#7714)
Age-check errors previously blocked Buzz startup. Normal app startup now
proceeds immediately; only a valid under-18 response from the current
launch request can restrict access.

Age check enforcement remains disabled at compile time by default.
Builds can opt in with `--dart-define=BUZZ_AGE_GATING_ENABLED=true`.

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
2026-09-17 16:44:08 -07:00

139 lines
6.0 KiB
Swift

import Foundation
import Testing
@testable import BuzzPushKit
struct BuzzAgeRestrictionSessionTests {
private func directory() throws -> URL {
let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
return url
}
@Test func confirmedRestrictionIsReleasedWithoutAStorageWrite() throws {
let url = try directory()
defer { try? FileManager.default.removeItem(at: url) }
let session = BuzzAgeRestrictionSession()
#expect(!BuzzAgeRestrictionSession.isRestricted(containerURL: url))
try session.restrict(containerURL: url)
#expect(BuzzAgeRestrictionSession.isRestricted(containerURL: url))
try session.restrict(containerURL: url)
session.release()
#expect(!BuzzAgeRestrictionSession.isRestricted(containerURL: url))
}
@Test func missingOrStaleReadOnlyFilesCannotRestrict() throws {
let url = try directory()
defer { try? FileManager.default.removeItem(at: url) }
#expect(!BuzzAgeRestrictionSession.isRestricted(containerURL: nil))
#expect(!BuzzAgeRestrictionSession.isRestricted(containerURL: url))
let file = url.appendingPathComponent(BuzzAgeRestrictionSession.fileName)
try Data("old or malformed restriction".utf8).write(to: file)
try FileManager.default.setAttributes([.posixPermissions: 0o400], ofItemAtPath: file.path)
#expect(!BuzzAgeRestrictionSession.isRestricted(containerURL: url))
}
@Test func acquisitionFailureCannotCreateAuthority() {
let missing = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
let session = BuzzAgeRestrictionSession()
#expect(throws: (any Error).self) { try session.restrict(containerURL: missing) }
#expect(!BuzzAgeRestrictionSession.isRestricted(containerURL: missing))
}
@Test func `Pending restriction prevents newer handoffs before the current delivery finishes`() throws {
let url = try directory()
defer { try? FileManager.default.removeItem(at: url) }
let session = BuzzAgeRestrictionSession()
var delivered = false
let allowed = BuzzAgeRestrictionSession.handoffIfAllowed(containerURL: url) {
#expect(throws: (any Error).self) { try session.restrict(containerURL: url) }
#expect(BuzzAgeRestrictionSession.isRestricted(containerURL: url))
#expect(!BuzzAgeRestrictionSession.handoffIfAllowed(containerURL: url) {
Issue.record("Pending restriction must prevent a newer handoff")
})
delivered = true
}
#expect(allowed)
#expect(delivered)
try session.restrict(containerURL: url)
#expect(!BuzzAgeRestrictionSession.handoffIfAllowed(containerURL: url) {
Issue.record("A confirmed restriction must suppress ordinary content")
})
session.release()
}
@Test func `Allowed restoration does not wait for a suspended handoff`() throws {
let url = try directory()
defer { try? FileManager.default.removeItem(at: url) }
let session = BuzzAgeRestrictionSession()
let allowed = BuzzAgeRestrictionSession.handoffIfAllowed(containerURL: url) {
#expect(throws: (any Error).self) { try session.restrict(containerURL: url) }
session.release()
#expect(!BuzzAgeRestrictionSession.isRestricted(containerURL: url))
#expect(BuzzAgeRestrictionSession.handoffIfAllowed(containerURL: url) {})
}
#expect(allowed)
}
@Test func `Handoff delivers when restriction storage is unavailable`() {
var deliveries = 0
#expect(BuzzAgeRestrictionSession.handoffIfAllowed(containerURL: nil) { deliveries += 1 })
let missing = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
#expect(BuzzAgeRestrictionSession.handoffIfAllowed(containerURL: missing) { deliveries += 1 })
#expect(deliveries == 2)
}
#if os(macOS)
@Test(.timeLimit(.minutes(1))) func processExitReleasesAuthorityWithoutCleanup() throws {
let url = try directory()
defer { try? FileManager.default.removeItem(at: url) }
let process = Process()
let input = Pipe()
let output = Pipe()
process.executableURL = URL(fileURLWithPath: "/usr/bin/python3")
process.arguments = ["-c", """
import fcntl, os, sys
fd = os.open(sys.argv[1], os.O_CREAT | os.O_RDWR, 0o600)
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
os.write(1, b'1')
os.read(0, 1)
os._exit(0)
""", url.appendingPathComponent(BuzzAgeRestrictionSession.fileName).path]
process.standardInput = input
process.standardOutput = output
try process.run()
defer { if process.isRunning { process.terminate() } }
#expect(try output.fileHandleForReading.read(upToCount: 1) == Data("1".utf8))
#expect(BuzzAgeRestrictionSession.isRestricted(containerURL: url))
try input.fileHandleForWriting.close()
process.waitUntilExit()
#expect(process.terminationStatus == 0)
#expect(!BuzzAgeRestrictionSession.isRestricted(containerURL: url))
#expect(FileManager.default.fileExists(atPath: url.appendingPathComponent(BuzzAgeRestrictionSession.fileName).path))
}
@Test(.timeLimit(.minutes(1))) func nativeRestrictionIsVisibleToAnotherProcess() throws {
let url = try directory()
defer { try? FileManager.default.removeItem(at: url) }
let session = BuzzAgeRestrictionSession()
try session.restrict(containerURL: url)
defer { session.release() }
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/python3")
process.arguments = ["-c", """
import fcntl, os, sys
fd = os.open(sys.argv[1], os.O_RDONLY)
try:
fcntl.flock(fd, fcntl.LOCK_SH | fcntl.LOCK_NB)
except BlockingIOError:
sys.exit(0)
sys.exit(1)
""", url.appendingPathComponent(BuzzAgeRestrictionSession.fileName).path]
try process.run()
defer { if process.isRunning { process.terminate() } }
process.waitUntilExit()
#expect(process.terminationStatus == 0)
}
#endif
}