|
| 1 | +import Darwin |
| 2 | +import Foundation |
| 3 | +import Testing |
| 4 | +@testable import Blitz |
| 5 | + |
| 6 | +@Test func decodeLiveEventRejectsInvalidTapPayload() { |
| 7 | + let json = """ |
| 8 | + { |
| 9 | + "v": 1, |
| 10 | + "id": "evt-1", |
| 11 | + "tsMs": 1774853265123, |
| 12 | + "source": { "client": "test" }, |
| 13 | + "target": { "platform": "ios", "deviceId": "SIM-1" }, |
| 14 | + "kind": "tap", |
| 15 | + "referenceWidth": 393, |
| 16 | + "referenceHeight": 852 |
| 17 | + } |
| 18 | + """.data(using: .utf8)! |
| 19 | + |
| 20 | + #expect(GestureVisualizationEvent.decodeLiveEvent(from: json) == nil) |
| 21 | +} |
| 22 | + |
| 23 | +@MainActor |
| 24 | +@Test func gestureSocketServiceReceivesLiveEventsAndDedupesByID() async throws { |
| 25 | + let root = URL(fileURLWithPath: "/tmp/gv-\(UUID().uuidString.prefix(8))", isDirectory: true) |
| 26 | + try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) |
| 27 | + defer { try? FileManager.default.removeItem(at: root) } |
| 28 | + |
| 29 | + let socketURL = root.appendingPathComponent("gesture-events.sock") |
| 30 | + let service = GestureVisualizationSocketService(socketURL: socketURL) |
| 31 | + defer { service.tearDown() } |
| 32 | + |
| 33 | + let tapEvent: [String: Any] = [ |
| 34 | + "v": 1, |
| 35 | + "id": "evt-1", |
| 36 | + "tsMs": 1_774_853_265_123 as Int64, |
| 37 | + "source": ["client": "test-client"], |
| 38 | + "target": ["platform": "ios", "deviceId": "SIM-1"], |
| 39 | + "kind": "tap", |
| 40 | + "x": 120, |
| 41 | + "y": 240, |
| 42 | + "referenceWidth": 393, |
| 43 | + "referenceHeight": 852, |
| 44 | + ] |
| 45 | + |
| 46 | + try sendDatagram(jsonObject: tapEvent, to: socketURL) |
| 47 | + try sendDatagram(jsonObject: tapEvent, to: socketURL) |
| 48 | + |
| 49 | + for _ in 0..<50 where service.liveEvents.isEmpty { |
| 50 | + try await Task.sleep(for: .milliseconds(10)) |
| 51 | + } |
| 52 | + |
| 53 | + #expect(service.liveEvents.count == 1) |
| 54 | + #expect(service.events(for: "SIM-1").count == 1) |
| 55 | + #expect(service.events(for: "OTHER").isEmpty) |
| 56 | +} |
| 57 | + |
| 58 | +private func sendDatagram(jsonObject: [String: Any], to socketURL: URL) throws { |
| 59 | + let data = try JSONSerialization.data(withJSONObject: jsonObject) |
| 60 | + let fd = socket(AF_UNIX, SOCK_DGRAM, 0) |
| 61 | + #expect(fd >= 0) |
| 62 | + defer { Darwin.close(fd) } |
| 63 | + |
| 64 | + var address = try makeSocketAddress(path: socketURL.path) |
| 65 | + let addressLength = socklen_t(address.sun_len) |
| 66 | + let sent = data.withUnsafeBytes { buffer in |
| 67 | + withUnsafePointer(to: &address) { pointer in |
| 68 | + pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) { sockaddrPointer in |
| 69 | + sendto(fd, buffer.baseAddress, buffer.count, 0, sockaddrPointer, addressLength) |
| 70 | + } |
| 71 | + } |
| 72 | + } |
| 73 | + |
| 74 | + #expect(sent == data.count) |
| 75 | +} |
| 76 | + |
| 77 | +private func makeSocketAddress(path: String) throws -> sockaddr_un { |
| 78 | + var address = sockaddr_un() |
| 79 | + let maxPathLength = MemoryLayout.size(ofValue: address.sun_path) - 1 |
| 80 | + guard path.utf8.count <= maxPathLength else { |
| 81 | + struct PathError: Error {} |
| 82 | + throw PathError() |
| 83 | + } |
| 84 | + |
| 85 | + address.sun_len = UInt8(MemoryLayout<sockaddr_un>.size) |
| 86 | + address.sun_family = sa_family_t(AF_UNIX) |
| 87 | + withUnsafeMutableBytes(of: &address.sun_path) { destination in |
| 88 | + path.withCString { source in |
| 89 | + destination.copyBytes(from: UnsafeRawBufferPointer(start: source, count: path.utf8.count + 1)) |
| 90 | + } |
| 91 | + } |
| 92 | + return address |
| 93 | +} |
0 commit comments