mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-08 10:44:50 +00:00
213 lines
9.1 KiB
Swift
213 lines
9.1 KiB
Swift
import Foundation
|
|
import Testing
|
|
|
|
@testable import ShadowsocksX_NG2
|
|
|
|
extension SubscriptionAutoUpdateWorkflowTests {
|
|
@Test func inFlightSettingsUseLatestModeAndCancellationPreservesPlan() async throws {
|
|
try await withAutoUpdateFixture { fixture in
|
|
let first = try await fixture.create(settings: .init(userOverride: .daily))
|
|
let fetcher = GatedFetcher()
|
|
let workflow = fixture.makeWorkflow(fetcher: fetcher)
|
|
let request = Task { await workflow.refreshSubscription(first.id) }
|
|
try await withTestTask(request, release: fetcher.releaseAll) {
|
|
try await requireTestCondition("fetch entered") { fetcher.didEnterFetch }
|
|
await workflow.refreshSubscription(first.id)
|
|
#expect(fetcher.requestCount == 1)
|
|
fixture.now = fixture.now.addingTimeInterval(100)
|
|
try await workflow.saveSubscription(
|
|
first.id, urlString: "https://example.com/sub", autoUpdate: .init(userOverride: .weekly))
|
|
fixture.now = fixture.now.addingTimeInterval(200)
|
|
}
|
|
#expect(
|
|
workflow.subscriptions.first?.autoUpdate.nextUpdateAt
|
|
== fixture.now.addingTimeInterval(604_800))
|
|
let cancelling = GatedFetcher()
|
|
let reloaded = fixture.makeWorkflow(fetcher: cancelling)
|
|
let before = reloaded.subscriptions
|
|
let cancelled = Task { await reloaded.refreshSubscription(first.id) }
|
|
try await withTestTask(cancelled, release: cancelling.releaseAll) {
|
|
try await requireTestCondition("cancelled fetch entered") { cancelling.didEnterFetch }
|
|
cancelled.cancel()
|
|
}
|
|
#expect(reloaded.subscriptions == before)
|
|
}
|
|
}
|
|
|
|
@Test func changingURLRejectsOldResponseAndClearsPolicyEvenWhenNewFetchFails() async throws {
|
|
try await withAutoUpdateFixture { fixture in
|
|
fixture.respond(mode: .daily, allowsOverride: false)
|
|
let first = try await fixture.create(
|
|
settings: .init(userOverride: .weekly, interval: .weekly))
|
|
let fetcher = URLChangeFetcher()
|
|
let workflow = fixture.makeWorkflow(fetcher: fetcher)
|
|
let request = Task { await workflow.refreshSubscription(first.id) }
|
|
var current: [SubscriptionSummary] = []
|
|
try await withTestTask(request, release: fetcher.release.release) {
|
|
try #require(await fetcher.entered.wait())
|
|
try await workflow.saveSubscription(first.id, urlString: "https://new.example.com/sub")
|
|
#expect(workflow.subscriptions.first?.id == first.id)
|
|
#expect(workflow.subscriptions.first?.groupID == first.groupID)
|
|
#expect(workflow.subscriptions.first?.autoUpdate.provider == nil)
|
|
#expect(workflow.subscriptions.first?.autoUpdate.userOverride == .weekly)
|
|
#expect(workflow.subscriptions.first?.lastSucceededAt == first.lastSucceededAt)
|
|
current = workflow.subscriptions
|
|
}
|
|
#expect(workflow.subscriptions == current)
|
|
#expect(try workflow.subscriptionURL(for: first.id) == "https://new.example.com/sub")
|
|
}
|
|
}
|
|
|
|
@Test func deletionRejectsLateResponse() async throws {
|
|
try await withAutoUpdateFixture { fixture in
|
|
let first = try await fixture.create()
|
|
let fetcher = GatedFetcher()
|
|
let workflow = fixture.makeWorkflow(fetcher: fetcher)
|
|
let request = Task { await workflow.refreshSubscription(first.id) }
|
|
try await withTestTask(request, release: fetcher.releaseAll) {
|
|
try await requireTestCondition("deleting fetch entered") { fetcher.didEnterFetch }
|
|
_ = try await workflow.removeSubscription(first.id)
|
|
}
|
|
#expect(workflow.subscriptions.isEmpty)
|
|
#expect(workflow.refreshingSubscriptionIDs.isEmpty)
|
|
}
|
|
}
|
|
|
|
@Test func dueSubscriptionsRunSequentiallyAndOverlapWithManualRefreshIsSkipped() async throws {
|
|
try await withAutoUpdateFixture { fixture in
|
|
let first = try await fixture.create(settings: .init(userOverride: .daily))
|
|
_ = try await fixture.create(settings: .init(userOverride: .daily))
|
|
fixture.now = fixture.now.addingTimeInterval(100_000)
|
|
let fetcher = GatedFetcher()
|
|
let workflow = fixture.makeWorkflow(fetcher: fetcher)
|
|
defer {
|
|
workflow.stopSubscriptionAutomaticUpdates()
|
|
fetcher.releaseAll()
|
|
}
|
|
workflow.startSubscriptionAutomaticUpdates()
|
|
try await requireTestCondition("batch fetch entered") { fetcher.didEnterFetch }
|
|
#expect(fetcher.requestCount == 1)
|
|
await workflow.refreshSubscription(first.id)
|
|
#expect(fetcher.requestCount == 1)
|
|
fetcher.releaseAll()
|
|
try await requireTestCondition("automatic batch completed") {
|
|
fetcher.requestCount == 2 && workflow.refreshingSubscriptionIDs.isEmpty
|
|
}
|
|
#expect(fetcher.requestCount == 2)
|
|
}
|
|
}
|
|
|
|
@Test func subscriptionsBecomingDueDuringABatchAreNotMissed() async throws {
|
|
try await withAutoUpdateFixture { fixture in
|
|
_ = try await fixture.create(settings: .init(userOverride: .daily))
|
|
fixture.now = fixture.now.addingTimeInterval(1_000)
|
|
_ = try await fixture.create(settings: .init(userOverride: .daily))
|
|
fixture.now = fixture.now.addingTimeInterval(85_400)
|
|
let fetcher = GatedFetcher()
|
|
let workflow = fixture.makeWorkflow(fetcher: fetcher)
|
|
defer {
|
|
workflow.stopSubscriptionAutomaticUpdates()
|
|
fetcher.releaseAll()
|
|
}
|
|
workflow.startSubscriptionAutomaticUpdates()
|
|
try await requireTestCondition("first due fetch entered") { fetcher.didEnterFetch }
|
|
#expect(fetcher.requestCount == 1)
|
|
fixture.now = fixture.now.addingTimeInterval(1_000)
|
|
fetcher.releaseAll()
|
|
try await requireTestCondition("automatic batch completed") {
|
|
fetcher.requestCount == 2 && workflow.refreshingSubscriptionIDs.isEmpty
|
|
}
|
|
#expect(fetcher.requestCount == 2)
|
|
}
|
|
}
|
|
|
|
@Test func schedulerStartsWithOverduePlanAndStopsBeforeFurtherRequests() async throws {
|
|
try await withAutoUpdateFixture { fixture in
|
|
_ = try await fixture.create(settings: .init(userOverride: .daily))
|
|
fixture.now = fixture.now.addingTimeInterval(1_000_000)
|
|
let fetcher = GatedFetcher()
|
|
let scheduled = TestAsyncGate("next automatic update scheduled")
|
|
let workflow = fixture.makeWorkflow(
|
|
fetcher: fetcher,
|
|
wait: { delay in
|
|
if delay > 0 {
|
|
scheduled.release()
|
|
try await Task.sleep(for: .seconds(delay))
|
|
}
|
|
})
|
|
defer {
|
|
fetcher.releaseAll()
|
|
workflow.stopSubscriptionAutomaticUpdates()
|
|
}
|
|
workflow.startSubscriptionAutomaticUpdates()
|
|
try await requireTestCondition("automatic fetch entered") { fetcher.didEnterFetch }
|
|
workflow.subscriptionAutomaticUpdatesDidWake()
|
|
#expect(fetcher.requestCount == 1)
|
|
fetcher.releaseAll()
|
|
try #require(await scheduled.wait())
|
|
#expect(
|
|
workflow.subscriptions.first?.autoUpdate.nextUpdateAt
|
|
== fixture.now.addingTimeInterval(86_400))
|
|
workflow.stopSubscriptionAutomaticUpdates()
|
|
fixture.now = fixture.now.addingTimeInterval(1_000_000)
|
|
workflow.subscriptionAutomaticUpdatesDidWake()
|
|
#expect(fetcher.requestCount == 1)
|
|
}
|
|
}
|
|
|
|
@Test func timerAndWakeRefreshOnlyAfterThePersistedDeadline() async throws {
|
|
try await withAutoUpdateFixture { fixture in
|
|
_ = try await fixture.create(settings: .init(userOverride: .daily))
|
|
let sleeping = TestAsyncGate("scheduler waiting for deadline")
|
|
let advance = TestAsyncGate("clock advances")
|
|
let fetcher = GatedFetcher()
|
|
let workflow = fixture.makeWorkflow(
|
|
fetcher: fetcher,
|
|
wait: { _ in
|
|
sleeping.release()
|
|
_ = await advance.wait()
|
|
})
|
|
defer {
|
|
advance.release()
|
|
fetcher.releaseAll()
|
|
workflow.stopSubscriptionAutomaticUpdates()
|
|
}
|
|
workflow.startSubscriptionAutomaticUpdates()
|
|
try #require(await sleeping.wait())
|
|
#expect(fetcher.requestCount == 0)
|
|
fixture.now = fixture.now.addingTimeInterval(86_400)
|
|
advance.release()
|
|
try await requireTestCondition("timer fires at deadline") { fetcher.didEnterFetch }
|
|
workflow.stopSubscriptionAutomaticUpdates()
|
|
fetcher.releaseAll()
|
|
// A fresh workflow resumes the still-overdue persisted plan on wake.
|
|
let resumedFetcher = GatedFetcher()
|
|
let resumed = fixture.makeWorkflow(fetcher: resumedFetcher)
|
|
defer {
|
|
resumedFetcher.releaseAll()
|
|
resumed.stopSubscriptionAutomaticUpdates()
|
|
}
|
|
resumed.startSubscriptionAutomaticUpdates()
|
|
resumed.subscriptionAutomaticUpdatesDidWake()
|
|
try await requireTestCondition("wake fetch entered") { resumedFetcher.didEnterFetch }
|
|
#expect(resumedFetcher.requestCount == 1)
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct URLChangeFetcher: SubscriptionFetching {
|
|
let entered = TestAsyncGate("old URL request entered")
|
|
let release = TestAsyncGate("old URL response released")
|
|
|
|
func fetch(_ url: URL) async throws -> Data {
|
|
if url.host == "new.example.com" { throw SubscriptionFetchError.httpStatus(code: 503) }
|
|
entered.release()
|
|
_ = await release.wait()
|
|
return Data(
|
|
#"""
|
|
{"version":1,"servers":[],"x_shadowsocksx_ng":{"schema_version":1,
|
|
"auto_update":{"mode":"daily","allow_user_override":false}}}
|
|
"""#.utf8)
|
|
}
|
|
}
|