Files

191 lines
6.3 KiB
Swift

import Foundation
import Testing
@testable import ShadowsocksX_NG2
@MainActor
struct ServerConnectivityTests {
@Test func repeatedRequestsShareWorkAndCoolingDoesNotAddHistory() async {
let id = NodeID(rawValue: UUID().uuidString)
var calls = 0
var now = Date(timeIntervalSince1970: 100)
let module = ServerConnectivity(
configuration: { _ in .fixture }, now: { now },
measure: { _, _, _ in
calls += 1
await Task.yield()
return .success(milliseconds: 42)
})
let batch = module.test([id, id])
_ = module.test([id])
await module.wait(for: batch)
#expect(calls == 1)
#expect(module.history(for: id).count == 1)
let skipped = module.test([id])
#expect(module.batch(skipped)?.cooling == [id])
#expect(module.history(for: id).count == 1)
now.addTimeInterval(10)
await module.wait(for: module.test([id]))
#expect(calls == 2)
}
@Test func overlappingBatchesShareOneSlotUntilCleanupCompletes() async {
let ids = (0..<7).map { _ in NodeID.fresh() }
let gate = AsyncGate()
var active = 0
var peak = 0
var calls = 0
let module = ServerConnectivity(
configuration: { _ in .fixture },
measure: { _, _, _ in
active += 1
calls += 1
peak = max(peak, active)
await gate.wait()
active -= 1
return .success(milliseconds: 12)
})
let firstTarget = module.test(Array(ids.prefix(5)))
let secondTarget = module.test(Array(ids.suffix(5)))
for _ in 0..<100 { await Task.yield() }
#expect(calls == 1)
#expect(module.testing.count == 7)
gate.release()
await module.wait(for: firstTarget)
await module.wait(for: secondTarget)
#expect(peak == 1)
#expect(calls == 7)
#expect(ids.allSatisfy { module.history(for: $0).count == 1 })
}
@Test func credentialValueChangesSuppressLateResultsAndPreserveCooldown() async {
let id = NodeID.fresh()
var server = SslocalServerDocument.fixture
let gate = AsyncGate()
let module = ServerConnectivity(
configuration: { _ in server },
measure: { _, _, _ in
await gate.wait()
return .success(milliseconds: 9)
})
let batch = module.test([id])
await Task.yield()
server = SslocalServerDocument(
id: "test", server: "example.com", serverPort: 443,
password: "changed", method: "aes-256-gcm", plugin: nil, pluginOpts: nil)
module.configurationsChanged()
gate.release()
await module.wait(for: batch)
#expect(module.history(for: id).isEmpty)
#expect(module.isCooling(id))
module.setURL(URL(string: "https://example.com/check")!)
#expect(module.isCooling(id))
}
@Test func latestFailureReplacesSuccessAndHistoryIsBounded() async {
let id = NodeID.fresh()
var now = Date(timeIntervalSince1970: 100)
var outcome = ServerConnectivity.Outcome.success(milliseconds: 11)
let module = ServerConnectivity(
configuration: { _ in .fixture }, now: { now },
measure: { _, _, _ in outcome })
await module.wait(for: module.test([id]))
outcome = .http(302)
for _ in 0..<12 {
now.addTimeInterval(10)
await module.wait(for: module.test([id]))
}
#expect(module.history(for: id).count == 10)
#expect(module.history(for: id).first?.outcome == .http(302))
let summary = module.summary(for: [id], at: now)
#expect(summary.hasResults)
#expect(summary.record == nil)
#expect(!summary.isTesting)
}
@Test func freshResultsBeatStaleAndTiesUseCatalogOrder() async {
let ids = (0..<3).map { _ in NodeID.fresh() }
var now = Date(timeIntervalSince1970: 100)
var latency = 1
let module = ServerConnectivity(
configuration: { _ in .fixture }, now: { now },
measure: { _, _, _ in .success(milliseconds: latency) })
await module.wait(for: module.test([ids[0]]))
now.addTimeInterval(300)
latency = 20
await module.wait(for: module.test(Array(ids.suffix(2))))
#expect(module.summary(for: ids, at: now).serverID == ids[1])
module.networkChanged()
#expect(module.summary(for: ids, at: now).serverID == ids[0])
#expect(module.summary(for: ids, at: now).record?.isExpired(at: now) == true)
module.setURL(URL(string: "https://example.com/new")!)
#expect(!module.summary(for: ids, at: now).hasResults)
}
@Test func urlAndNetworkChangesSuppressInFlightAndShutdownDrainsQueuedWork() async {
let ids = (0..<5).map { _ in NodeID.fresh() }
let gate = AsyncGate()
let module = ServerConnectivity(
configuration: { _ in .fixture },
measure: { _, _, _ in
await gate.wait()
return .success(milliseconds: 9)
})
let batch = module.test(ids)
await Task.yield()
module.networkChanged()
let shutdown = Task { await module.shutdown() }
await Task.yield()
#expect(!module.testing.isEmpty)
gate.release()
await shutdown.value
await module.wait(for: batch)
#expect(module.testing.isEmpty)
#expect(ids.allSatisfy { module.history(for: $0).isEmpty && module.isCooling($0) })
}
@Test func queuedConfigurationIsRecheckedBeforeLaunching() async {
let ids = (0..<4).map { _ in NodeID.fresh() }
let gate = AsyncGate()
var changed = false
var calls = 0
let module = ServerConnectivity(
configuration: { id in
if changed && id == ids[3] { throw ServerFormLoadError.notFound }
return .fixture
},
measure: { _, _, _ in
calls += 1
await gate.wait()
return .success(milliseconds: 5)
})
let batch = module.test(ids)
for _ in 0..<100 { await Task.yield() }
changed = true
gate.release()
await module.wait(for: batch)
#expect(calls == 3)
#expect(module.history(for: ids[3]).isEmpty)
#expect(module.isCooling(ids[3]))
}
@Test func urlValidationAllowsHTTPOnly() {
#expect(ServerConnectivity.validURL("http://example.com/check") != nil)
#expect(ServerConnectivity.validURL("https://example.com/check?q=secret") == nil)
for value in [
"file:///tmp/check", "ftp://example.com", "https:///", "example.com", "http://x/#fragment",
] {
#expect(ServerConnectivity.validURL(value) == nil)
}
}
}
extension SslocalServerDocument {
fileprivate static var fixture: Self {
Self(
id: "test", server: "example.com", serverPort: 443, password: "secret",
method: "aes-256-gcm", plugin: nil, pluginOpts: nil)
}
}