158 lines
5.0 KiB
Swift
158 lines
5.0 KiB
Swift
import Testing
|
||
import Foundation
|
||
@testable import Mobile_Music_Assistant
|
||
|
||
// MARK: - ConnectionState
|
||
|
||
@Suite("MAWebSocketClient – ConnectionState")
|
||
struct ConnectionStateTests {
|
||
|
||
@Test("Fresh client starts disconnected")
|
||
func startsDisconnected() {
|
||
let client = MAWebSocketClient()
|
||
if case .disconnected = client.connectionState {
|
||
// pass
|
||
} else {
|
||
Issue.record("Expected .disconnected, got \(client.connectionState)")
|
||
}
|
||
}
|
||
|
||
@Test("isConnected is false when disconnected")
|
||
func isConnectedFalseWhenDisconnected() {
|
||
let client = MAWebSocketClient()
|
||
#expect(client.isConnected == false)
|
||
}
|
||
|
||
@Test("ConnectionState descriptions are distinct")
|
||
func statesAreDistinct() {
|
||
// Regression: ensure no two states resolve to the same string representation
|
||
let states: [MAWebSocketClient.ConnectionState] = [
|
||
.disconnected,
|
||
.connecting,
|
||
.connected,
|
||
.reconnecting(attempt: 1),
|
||
.reconnecting(attempt: 2)
|
||
]
|
||
let descriptions = states.map { "\($0)" }
|
||
let unique = Set(descriptions)
|
||
#expect(unique.count == descriptions.count)
|
||
}
|
||
}
|
||
|
||
// MARK: - Reconnection Backoff
|
||
|
||
@Suite("MAWebSocketClient – Reconnection Backoff")
|
||
struct ReconnectionBackoffTests {
|
||
|
||
/// Replicates the exact formula used in MAWebSocketClient.
|
||
private func backoffDelay(attempt: Int) -> Double {
|
||
let base = 3.0
|
||
let maxDelay = 30.0
|
||
return min(base * pow(2.0, Double(attempt - 1)), maxDelay)
|
||
}
|
||
|
||
@Test("Attempt 1 gives 3 seconds")
|
||
func attempt1() {
|
||
#expect(backoffDelay(attempt: 1) == 3.0)
|
||
}
|
||
|
||
@Test("Attempt 2 gives 6 seconds")
|
||
func attempt2() {
|
||
#expect(backoffDelay(attempt: 2) == 6.0)
|
||
}
|
||
|
||
@Test("Attempt 3 gives 12 seconds")
|
||
func attempt3() {
|
||
#expect(backoffDelay(attempt: 3) == 12.0)
|
||
}
|
||
|
||
@Test("Attempt 4 gives 24 seconds")
|
||
func attempt4() {
|
||
#expect(backoffDelay(attempt: 4) == 24.0)
|
||
}
|
||
|
||
@Test("Attempt 5 is capped at 30 seconds")
|
||
func attempt5Capped() {
|
||
#expect(backoffDelay(attempt: 5) == 30.0)
|
||
}
|
||
|
||
@Test("High attempt numbers stay capped at 30 seconds")
|
||
func highAttemptStaysCapped() {
|
||
for attempt in 6...20 {
|
||
#expect(backoffDelay(attempt: attempt) == 30.0)
|
||
}
|
||
}
|
||
|
||
@Test("Backoff is strictly increasing up to the cap")
|
||
func strictlyIncreasing() {
|
||
var previous = 0.0
|
||
for attempt in 1...5 {
|
||
let delay = backoffDelay(attempt: attempt)
|
||
#expect(delay > previous)
|
||
previous = delay
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - AnyCodable
|
||
|
||
@Suite("AnyCodable")
|
||
struct AnyCodableTests {
|
||
|
||
@Test("Bool encodes and decodes correctly")
|
||
func boolRoundTrip() throws {
|
||
let encoded = try JSONEncoder().encode(AnyCodable(true))
|
||
let decoded = try JSONDecoder().decode(AnyCodable.self, from: encoded)
|
||
#expect(try decoded.decode(as: Bool.self) == true)
|
||
}
|
||
|
||
@Test("Int encodes and decodes correctly")
|
||
func intRoundTrip() throws {
|
||
let encoded = try JSONEncoder().encode(AnyCodable(42))
|
||
let decoded = try JSONDecoder().decode(AnyCodable.self, from: encoded)
|
||
#expect(try decoded.decode(as: Int.self) == 42)
|
||
}
|
||
|
||
@Test("String encodes and decodes correctly")
|
||
func stringRoundTrip() throws {
|
||
let encoded = try JSONEncoder().encode(AnyCodable("hello"))
|
||
let decoded = try JSONDecoder().decode(AnyCodable.self, from: encoded)
|
||
#expect(try decoded.decode(as: String.self) == "hello")
|
||
}
|
||
|
||
@Test("Double encodes and decodes correctly")
|
||
func doubleRoundTrip() throws {
|
||
let encoded = try JSONEncoder().encode(AnyCodable(3.14))
|
||
let decoded = try JSONDecoder().decode(AnyCodable.self, from: encoded)
|
||
let value = try decoded.decode(as: Double.self)
|
||
#expect(abs(value - 3.14) < 0.001)
|
||
}
|
||
|
||
@Test("Array encodes and decodes correctly")
|
||
func arrayRoundTrip() throws {
|
||
let arr = AnyCodable(["a", "b", "c"])
|
||
let encoded = try JSONEncoder().encode(arr)
|
||
let decoded = try JSONDecoder().decode(AnyCodable.self, from: encoded)
|
||
let values = try decoded.decode(as: [AnyCodable].self)
|
||
#expect(values.count == 3)
|
||
}
|
||
|
||
@Test("decode(as:) throws for wrong type")
|
||
func decodeThrowsForWrongType() throws {
|
||
let encoded = try JSONEncoder().encode(AnyCodable("text"))
|
||
let decoded = try JSONDecoder().decode(AnyCodable.self, from: encoded)
|
||
#expect(throws: (any Error).self) {
|
||
try decoded.decode(as: Int.self)
|
||
}
|
||
}
|
||
|
||
@Test("Dictionary decodes keys correctly")
|
||
func dictRoundTrip() throws {
|
||
// Build the AnyCodable from raw JSON to avoid double-wrapping issues
|
||
let json = #"{"key":"value"}"#
|
||
let decoded = try JSONDecoder().decode(AnyCodable.self, from: Data(json.utf8))
|
||
let map = try decoded.decode(as: [String: AnyCodable].self)
|
||
#expect(try map["key"]?.decode(as: String.self) == "value")
|
||
}
|
||
}
|