Add native SwiftUI player and macOS CI
iOS build and tests / simulator (push) Has been cancelled

This commit is contained in:
2026-09-10 09:11:55 +03:00
parent 6307070194
commit 6174be5610
20 changed files with 1729 additions and 0 deletions
+67
View File
@@ -0,0 +1,67 @@
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 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"])
}
}
+22
View File
@@ -0,0 +1,22 @@
{
"protocolVersion": 1,
"timestamp": "2026-09-09T00:00:00+00:00",
"hasSession": true,
"title": "Название",
"artist": "Исполнитель",
"source": "идентификатор приложения",
"playbackStatus": "Playing",
"positionSeconds": 42.5,
"startSeconds": 0,
"endSeconds": 180,
"minSeekSeconds": 0,
"maxSeekSeconds": 180,
"playbackRate": 1,
"capabilities": {"play": false, "pause": true, "toggle": true, "next": true, "previous": true, "seek": true},
"volume": 0.5,
"muted": false,
"mediaError": null,
"volumeError": null,
"artworkId": null,
"artworkError": null
}