86 lines
4.7 KiB
Swift
86 lines
4.7 KiB
Swift
import XCTest
|
|
@testable import MusicBridge
|
|
|
|
final class ProtocolTests: XCTestCase {
|
|
private let fingerprint = String(repeating: "A", count: 64)
|
|
|
|
func testPrivateAddresses() throws {
|
|
for address in ["10.0.0.1", "172.16.0.1", "172.31.255.254", "192.168.1.10"] {
|
|
XCTAssertTrue(Connection.isPrivateIPv4(address))
|
|
}
|
|
for address in ["127.0.0.1", "8.8.8.8", "172.32.0.1", "169.254.1.2", "192.168.1.999", "192.168.01.1", "localhost", "::1"] {
|
|
XCTAssertFalse(Connection.isPrivateIPv4(address))
|
|
}
|
|
}
|
|
|
|
func testRejectsCredentialOrPathInjection() {
|
|
for address in ["http://192.168.1.2", "https://user@192.168.1.2", "https://192.168.1.2/path",
|
|
"https://192.168.1.2?token=secret", "https://192.168.1.2#fragment", "https://192.168.1.2:0"] {
|
|
XCTAssertThrowsError(try Connection(endpoint: address, fingerprint: fingerprint))
|
|
}
|
|
}
|
|
|
|
func testFingerprintValidation() throws {
|
|
XCTAssertThrowsError(try Connection(endpoint: "https://192.168.1.2", fingerprint: String(repeating: "Z", count: 64)))
|
|
XCTAssertThrowsError(try Connection(endpoint: "https://192.168.1.2", fingerprint: "AA"))
|
|
XCTAssertEqual(try Connection(endpoint: "https://192.168.1.2:8765", fingerprint: fingerprint.lowercased()).fingerprint, fingerprint)
|
|
}
|
|
|
|
func testInvitationAndExpiry() throws {
|
|
let now = Date(timeIntervalSince1970: 1_800_000_000)
|
|
let payload: [String: Any] = ["version": 1, "endpoint": "https://192.168.1.10:8765",
|
|
"certificateSha256": fingerprint, "code": "01234567",
|
|
"expiresAt": ISO8601DateFormatter().string(from: now.addingTimeInterval(60))]
|
|
let data = try JSONSerialization.data(withJSONObject: payload).base64EncodedString()
|
|
.replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "_").replacingOccurrences(of: "=", with: "")
|
|
let text = "musicbridge://pair?data=\(data)"
|
|
XCTAssertEqual(try Invitation.parse(text, now: now).code, "01234567")
|
|
XCTAssertThrowsError(try Invitation.parse(text, now: now.addingTimeInterval(61)))
|
|
XCTAssertThrowsError(try Invitation.parse(text + "&data=other", now: now))
|
|
}
|
|
|
|
func testCodeRequiresEightASCIIDigits() {
|
|
XCTAssertTrue(Invitation.validCode("01234567"))
|
|
XCTAssertFalse(Invitation.validCode("1234567"))
|
|
XCTAssertFalse(Invitation.validCode("12345678"))
|
|
}
|
|
|
|
func testWindowsInvitationTimestampAndMalformedJSON() throws {
|
|
// System.Text.Json emits up to seven fractional digits and a numeric UTC offset.
|
|
let json = """
|
|
{"version":1,"endpoint":"https://192.168.1.23:8765","certificateSha256":"\(fingerprint)","code":"01234567","expiresAt":"2026-09-12T15:30:00.1234567+03:00"}
|
|
"""
|
|
func payload(_ json: String) -> String {
|
|
"musicbridge://pair?data=" + Data(json.utf8).base64EncodedString()
|
|
.replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "_").replacingOccurrences(of: "=", with: "")
|
|
}
|
|
let before = try XCTUnwrap(ISO8601DateFormatter().date(from: "2026-09-12T12:29:00Z"))
|
|
XCTAssertEqual(try Invitation.parse(payload(json), now: before).code, "01234567")
|
|
XCTAssertThrowsError(try Invitation.parse(payload(json), now: before.addingTimeInterval(120)))
|
|
XCTAssertThrowsError(try Invitation.parse(payload("{}"), now: before)) { error in
|
|
XCTAssertTrue(error is BridgeError)
|
|
}
|
|
XCTAssertThrowsError(try Invitation.parse("https://example.com"))
|
|
}
|
|
|
|
func testWindowsSnapshotAndTimeline() throws {
|
|
let url = try XCTUnwrap(Bundle(for: Self.self).url(forResource: "state-v1", withExtension: "json"))
|
|
let state = try JSONDecoder().decode(MediaState.self, from: Data(contentsOf: url))
|
|
XCTAssertEqual(state.position(elapsed: 2), 44.5)
|
|
XCTAssertEqual(state.position(elapsed: 9999), 180)
|
|
XCTAssertTrue(state.capabilities.seek)
|
|
XCTAssertEqual(state.volume, 0.5)
|
|
XCTAssertNoThrow(try BridgeClient.validate(state))
|
|
}
|
|
|
|
func testCommandsCarryUniqueIDsAndOptionalValue() throws {
|
|
let command = Command(type: "seek", value: 90.5)
|
|
let object = try XCTUnwrap(JSONSerialization.jsonObject(with: JSONEncoder().encode(command)) as? [String: Any])
|
|
XCTAssertEqual(object["type"] as? String, "seek")
|
|
XCTAssertEqual(object["value"] as? Double, 90.5)
|
|
XCTAssertNotEqual(command.id, Command(type: "seek", value: 90.5).id)
|
|
let toggle = try XCTUnwrap(JSONSerialization.jsonObject(with: JSONEncoder().encode(Command(type: "toggle", value: nil))) as? [String: Any])
|
|
XCTAssertNil(toggle["value"])
|
|
}
|
|
}
|