Files
Sybil-2/ios/Packages/Sybil/Tests/SybilTests/AgentPushAPITests.swift
T

89 lines
5.5 KiB
Swift

import Foundation
import Testing
@testable import Sybil
private final class PushURLProtocol: URLProtocol, @unchecked Sendable {
static let lock = NSLock()
nonisolated(unsafe) private static var handler: (@Sendable (URLRequest) throws -> (Int, Data))?
static func install(_ value: @escaping @Sendable (URLRequest) throws -> (Int, Data)) { lock.withLock { handler = value } }
override class func canInit(with request: URLRequest) -> Bool { true }
override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }
override func startLoading() {
do {
let handler = Self.lock.withLock { Self.handler! }
let (status, data) = try handler(request)
client?.urlProtocol(self, didReceive: HTTPURLResponse(url: request.url!, statusCode: status, httpVersion: nil, headerFields: ["Location": "https://untrusted.test/"])!, cacheStoragePolicy: .notAllowed)
client?.urlProtocol(self, didLoad: data); client?.urlProtocolDidFinishLoading(self)
} catch { client?.urlProtocol(self, didFailWithError: error) }
}
override func stopLoading() {}
}
private func pushClient(_ url: String = "https://example.test/api", token: String? = "fixture-token") -> SybilAPIClient {
let config = URLSessionConfiguration.ephemeral; config.protocolClasses = [PushURLProtocol.self]
return SybilAPIClient(configuration: .init(baseURL: URL(string: url)!, authToken: token), session: URLSession(configuration: config))
}
private func pushRequestBody(_ request: URLRequest) -> Data {
if let body = request.httpBody { return body }
guard let stream = request.httpBodyStream else { return Data() }
stream.open(); defer { stream.close() }
var bytes = Data(), buffer = [UInt8](repeating: 0, count: 1024)
while stream.hasBytesAvailable { let count = stream.read(&buffer, maxLength: buffer.count); if count <= 0 { break }; bytes.append(contentsOf: buffer.prefix(count)) }
return bytes
}
@Suite(.serialized) struct AgentPushAPITests {
@Test func authenticatedBasePathRegistrationAndRevoke() async throws {
let id = UUID()
PushURLProtocol.install { request in
#expect(request.url?.host == "example.test")
#expect(request.url?.query == nil)
#expect(request.cachePolicy == .reloadIgnoringLocalCacheData)
#expect(request.value(forHTTPHeaderField: "Authorization") == "Bearer fixture-token")
#expect(request.value(forHTTPHeaderField: "Cache-Control") == "no-store")
#expect(request.timeoutInterval == 8)
if request.httpMethod == "GET" {
#expect(request.url?.path == "/api/v1/agent/push/config")
return (200, Data(#"{"version":1,"enabled":true,"apnsAvailable":true,"webPushAvailable":false,"sourceId":"11111111-1111-4111-8111-111111111111","quarantined":false,"policy":{"future":1}}"#.utf8))
}
#expect(request.url?.path == "/api/v1/agent/push/installations/" + id.uuidString.lowercased())
if request.httpMethod == "DELETE" { return (204, Data()) }
let object = try #require(JSONSerialization.jsonObject(with: pushRequestBody(request)) as? [String: Any])
#expect(Set(object.keys) == Set(["transport", "enabled", "token", "environment", "topic"]))
#expect(object["token"] as? String == "00010aff")
#expect(object["environment"] as? String == "production")
#expect(object["topic"] as? String == "net.buzzert.sybil2")
return (200, try JSONEncoder().encode(AgentPushReceipt(installationId: id.uuidString, transport: "apns", enabled: true, version: 7)))
}
let client = pushClient()
#expect(try await client.agentPushConfig().available)
#expect(try await client.agentRegisterPush(id: id, registration: .init(token: "00010aff", environment: .production)).version == 7)
try await client.agentRevokePush(id: id)
}
@Test func unsupportedRedirectAndReflectedSecretsAreNotExposed() async {
for status in [302, 401, 404, 409, 503] {
PushURLProtocol.install { _ in (status, Data(#"{"token":"do-not-reflect-this-token","message":"do-not-reflect-this-token"}"#.utf8)) }
do { _ = try await pushClient().agentPushConfig(); Issue.record("Unexpected success") }
catch { #expect(!error.localizedDescription.contains("do-not-reflect")) }
}
PushURLProtocol.install { _ in Issue.record("Unsafe request reached transport"); return (200, Data()) }
for client in [pushClient(token: nil), pushClient("https://example.test/api?token=bad"), pushClient("https://user:password@example.test")] {
do { _ = try await client.agentPushConfig(); Issue.record("Unsafe configuration accepted") }
catch { }
}
}
@Test func sourceAwareReadWritesAndLegacyFallback() async throws {
let source = "11111111-1111-4111-8111-111111111111"
for expectedSource: String? in [nil, source] {
PushURLProtocol.install { request in
#expect(request.url?.path == "/api/v1/agent/read-state")
let object = try #require(JSONSerialization.jsonObject(with: pushRequestBody(request)) as? [String: String])
#expect(object["cursor"] == "4" && object["sourceId"] == expectedSource)
return (200, try JSONEncoder().encode(AgentReadState(cursor: "4", sourceId: expectedSource)))
}
#expect(try await pushClient().agentMarkRead(cursor: "4", sourceID: expectedSource).sourceId == expectedSource)
}
}
}