mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-10-29 05:33:45 +00:00
refactor: centralize subscription automatic update transitions
This commit is contained in:
+14
-43
@@ -16,7 +16,7 @@ extension CatalogWorkflow {
|
||||
/// 成功)。返回订阅 summary(状态在首次刷新中落定)。
|
||||
@discardableResult
|
||||
func createSubscription(
|
||||
urlString: String, autoUpdate: SubscriptionAutoUpdate = SubscriptionAutoUpdate()
|
||||
urlString: String, autoUpdate: SubscriptionAutoUpdateSettings = .init()
|
||||
) async throws -> SubscriptionSummary {
|
||||
let url = try Self.validatedSubscriptionURL(urlString)
|
||||
var record = SubscriptionRecord(
|
||||
@@ -24,10 +24,8 @@ extension CatalogWorkflow {
|
||||
groupID: .fresh(),
|
||||
url: url.absoluteString,
|
||||
status: .never)
|
||||
record.autoUpdate.userOverride = autoUpdate.userOverride
|
||||
record.autoUpdate.interval = autoUpdate.interval == .weekly ? .weekly : .daily
|
||||
record.autoUpdate.schedule(
|
||||
from: dependencies.subscriptionNow(), jitter: dependencies.subscriptionJitter())
|
||||
record.autoUpdate.save(
|
||||
autoUpdate, at: dependencies.subscriptionNow(), jitter: dependencies.subscriptionJitter())
|
||||
try commitSubscriptionDocument { catalog, subscriptions in
|
||||
try catalog.addGroup(url.host ?? "", source: .subscription, id: record.groupID, now: Date())
|
||||
subscriptions.append(record)
|
||||
@@ -91,10 +89,10 @@ extension CatalogWorkflow {
|
||||
}
|
||||
}
|
||||
|
||||
/// 编辑订阅 URL:订阅/固定分组/远端身份
|
||||
/// 命名空间全部保留;最后成功快照保留到新 URL 刷新成功,随后立即刷新。
|
||||
func editSubscriptionURL(
|
||||
_ id: NodeID, urlString: String, autoUpdate: SubscriptionAutoUpdate? = nil
|
||||
/// 保存订阅地址与自动更新选择,保留订阅、固定分组与远端身份。
|
||||
/// 仅地址变化时立即刷新;设置保存不获取远端内容或触发运行时收敛。
|
||||
func saveSubscription(
|
||||
_ id: NodeID, urlString: String, autoUpdate: SubscriptionAutoUpdateSettings? = nil
|
||||
) async throws {
|
||||
guard let record = subscriptionRecord(withID: id) else {
|
||||
throw SubscriptionFormError.notFound
|
||||
@@ -105,42 +103,16 @@ extension CatalogWorkflow {
|
||||
guard let index = subscriptions.firstIndex(where: { $0.id == id }) else {
|
||||
throw SubscriptionFormError.notFound
|
||||
}
|
||||
let previousMode = subscriptions[index].autoUpdate.effectiveMode
|
||||
if let autoUpdate, subscriptions[index].autoUpdate.allowsUserOverride {
|
||||
subscriptions[index].autoUpdate.userOverride = autoUpdate.userOverride
|
||||
subscriptions[index].autoUpdate.interval = autoUpdate.interval == .weekly ? .weekly : .daily
|
||||
}
|
||||
subscriptions[index].autoUpdate.save(
|
||||
autoUpdate, sourceChanged: changedURL,
|
||||
at: dependencies.subscriptionNow(), jitter: dependencies.subscriptionJitter())
|
||||
subscriptions[index].url = url.absoluteString
|
||||
if changedURL { subscriptions[index].autoUpdate.provider = nil }
|
||||
if previousMode != subscriptions[index].autoUpdate.effectiveMode {
|
||||
subscriptions[index].autoUpdate.schedule(
|
||||
from: dependencies.subscriptionNow(), jitter: dependencies.subscriptionJitter())
|
||||
}
|
||||
}
|
||||
if changedURL { cancelSubscriptionRequest(id) }
|
||||
republishCommittedState()
|
||||
if changedURL { await refreshSubscription(record.id) }
|
||||
}
|
||||
|
||||
/// Settings-only saves never reconcile the proxy runtime or fetch the source.
|
||||
func saveSubscriptionAutoUpdate(_ id: NodeID, settings: SubscriptionAutoUpdate) throws {
|
||||
guard let record = subscriptionRecord(withID: id) else {
|
||||
throw SubscriptionFormError.notFound
|
||||
}
|
||||
guard record.autoUpdate.allowsUserOverride else { return }
|
||||
try dependencies.coordinator.commitSubscriptions { subscriptions in
|
||||
guard let index = subscriptions.firstIndex(where: { $0.id == id }) else { return }
|
||||
let previousMode = subscriptions[index].autoUpdate.effectiveMode
|
||||
subscriptions[index].autoUpdate.userOverride = settings.userOverride
|
||||
subscriptions[index].autoUpdate.interval = settings.interval == .weekly ? .weekly : .daily
|
||||
if previousMode != subscriptions[index].autoUpdate.effectiveMode {
|
||||
subscriptions[index].autoUpdate.schedule(
|
||||
from: dependencies.subscriptionNow(), jitter: dependencies.subscriptionJitter())
|
||||
}
|
||||
}
|
||||
republishCommittedState()
|
||||
}
|
||||
|
||||
func cancelSubscriptionRequest(_ id: NodeID) {
|
||||
subscriptionRequests.removeValue(forKey: id)?.task.cancel()
|
||||
setRefreshInFlight(id, false)
|
||||
@@ -208,9 +180,8 @@ extension CatalogWorkflow {
|
||||
subscriptions[index].information =
|
||||
snapshot.information.isEmpty ? nil : snapshot.information
|
||||
subscriptions[index].lastSucceededAt = now
|
||||
subscriptions[index].autoUpdate.apply(snapshot.autoUpdatePolicy)
|
||||
subscriptions[index].autoUpdate.schedule(
|
||||
from: now, jitter: dependencies.subscriptionJitter())
|
||||
subscriptions[index].autoUpdate.refreshSucceeded(
|
||||
policy: snapshot.autoUpdatePolicy, at: now, jitter: dependencies.subscriptionJitter())
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
@@ -228,8 +199,8 @@ extension CatalogWorkflow {
|
||||
if let index = subscriptions.firstIndex(where: { $0.id == subscriptionID }) {
|
||||
let now = dependencies.subscriptionNow()
|
||||
subscriptions[index].status = .failed(at: now, failure: failure)
|
||||
subscriptions[index].autoUpdate.schedule(
|
||||
from: now, jitter: dependencies.subscriptionJitter())
|
||||
subscriptions[index].autoUpdate.refreshFailed(
|
||||
at: now, jitter: dependencies.subscriptionJitter())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ struct ServersView: View {
|
||||
onCreated: selectCreatedNode)
|
||||
}
|
||||
.sheet(item: $subscriptionEditTarget) { summary in
|
||||
EditSubscriptionURLSheet(workflow: workflow, errors: errors, summary: summary)
|
||||
EditSubscriptionSheet(workflow: workflow, errors: errors, summary: summary)
|
||||
}
|
||||
.sheet(isPresented: $isPresentingAddSubscription) {
|
||||
AddSubscriptionSheet(workflow: workflow, errors: errors)
|
||||
|
||||
@@ -17,7 +17,7 @@ struct AddSubscriptionSheet: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@State private var urlString = ""
|
||||
@State private var autoUpdate = SubscriptionAutoUpdate()
|
||||
@State private var autoUpdate = SubscriptionAutoUpdateSettings()
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
@@ -55,16 +55,16 @@ struct AddSubscriptionSheet: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// 「编辑订阅 URL」表单(保留订阅与固定分组身份);保存后立即刷新。
|
||||
/// 订阅编辑表单(保留订阅与固定分组身份);仅地址变化时立即刷新。
|
||||
/// URL 明文经显式命令读取。
|
||||
struct EditSubscriptionURLSheet: View {
|
||||
struct EditSubscriptionSheet: View {
|
||||
let workflow: CatalogWorkflow
|
||||
let errors: ErrorAlertPresenter
|
||||
let summary: SubscriptionSummary
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@State private var urlString = ""
|
||||
@State private var autoUpdate = SubscriptionAutoUpdate()
|
||||
@State private var autoUpdate = SubscriptionAutoUpdateSettings()
|
||||
@State private var loaded = false
|
||||
|
||||
var body: some View {
|
||||
@@ -78,7 +78,7 @@ struct EditSubscriptionURLSheet: View {
|
||||
prompt: Text(verbatim: "https://example.com/subscription.json")
|
||||
.foregroundStyle(.placeholder)
|
||||
)
|
||||
SubscriptionAutoUpdateForm(settings: $autoUpdate)
|
||||
SubscriptionAutoUpdateForm(settings: $autoUpdate, provider: summary.autoUpdate.provider)
|
||||
HStack {
|
||||
Spacer()
|
||||
Button("取消", role: .cancel) { dismiss() }
|
||||
@@ -94,7 +94,7 @@ struct EditSubscriptionURLSheet: View {
|
||||
loaded = true
|
||||
do {
|
||||
urlString = try workflow.subscriptionURL(for: summary.id)
|
||||
autoUpdate = summary.autoUpdate
|
||||
autoUpdate = summary.autoUpdate.settings
|
||||
} catch {
|
||||
errors.present(error)
|
||||
}
|
||||
@@ -104,7 +104,7 @@ struct EditSubscriptionURLSheet: View {
|
||||
private func save() {
|
||||
Task {
|
||||
do {
|
||||
try await workflow.editSubscriptionURL(
|
||||
try await workflow.saveSubscription(
|
||||
summary.id, urlString: urlString, autoUpdate: autoUpdate)
|
||||
dismiss()
|
||||
} catch {
|
||||
@@ -115,12 +115,15 @@ struct EditSubscriptionURLSheet: View {
|
||||
}
|
||||
|
||||
private struct SubscriptionAutoUpdateForm: View {
|
||||
@Binding var settings: SubscriptionAutoUpdate
|
||||
@Binding var settings: SubscriptionAutoUpdateSettings
|
||||
var provider: SubscriptionUpdatePolicy?
|
||||
|
||||
private var providerMode: SubscriptionUpdateMode { provider?.mode ?? .off }
|
||||
|
||||
var body: some View {
|
||||
GroupBox("自动更新") {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
if settings.allowsUserOverride {
|
||||
if provider?.allowUserOverride != false {
|
||||
Picker(
|
||||
"设置来源",
|
||||
selection: Binding(
|
||||
@@ -146,10 +149,10 @@ private struct SubscriptionAutoUpdateForm: View {
|
||||
if settings.userOverride != .off { settings.userOverride = interval }
|
||||
}
|
||||
} else {
|
||||
Text(settings.effectiveMode.displayName).foregroundStyle(.secondary)
|
||||
Text(providerMode.displayName).foregroundStyle(.secondary)
|
||||
}
|
||||
} else {
|
||||
LabeledContent("供应商设置", value: settings.effectiveMode.displayName)
|
||||
LabeledContent("供应商设置", value: providerMode.displayName)
|
||||
Text("供应商禁止覆盖自动更新设置")
|
||||
.font(.callout).foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
@@ -27,12 +27,18 @@ enum SubscriptionUpdatePolicyResult: Equatable, Sendable {
|
||||
case valid(SubscriptionUpdatePolicy)
|
||||
}
|
||||
|
||||
/// Provider constraints hide, but never erase, the user's saved override.
|
||||
struct SubscriptionAutoUpdate: Codable, Equatable, Sendable {
|
||||
var provider: SubscriptionUpdatePolicy?
|
||||
/// Editable choices; provider facts and the persisted deadline are not form input.
|
||||
struct SubscriptionAutoUpdateSettings: Equatable, Sendable {
|
||||
var userOverride: SubscriptionUpdateMode?
|
||||
var interval: SubscriptionUpdateMode = .daily
|
||||
var nextUpdateAt: Date?
|
||||
}
|
||||
|
||||
/// Provider constraints hide, but never erase, the user's saved override.
|
||||
struct SubscriptionAutoUpdate: Codable, Equatable, Sendable {
|
||||
private(set) var provider: SubscriptionUpdatePolicy?
|
||||
private(set) var userOverride: SubscriptionUpdateMode?
|
||||
private(set) var interval: SubscriptionUpdateMode = .daily
|
||||
private(set) var nextUpdateAt: Date?
|
||||
|
||||
var allowsUserOverride: Bool { provider?.allowUserOverride != false }
|
||||
|
||||
@@ -41,15 +47,40 @@ struct SubscriptionAutoUpdate: Codable, Equatable, Sendable {
|
||||
return userOverride ?? provider?.mode ?? .off
|
||||
}
|
||||
|
||||
mutating func apply(_ result: SubscriptionUpdatePolicyResult) {
|
||||
switch result {
|
||||
var settings: SubscriptionAutoUpdateSettings {
|
||||
.init(userOverride: userOverride, interval: interval)
|
||||
}
|
||||
|
||||
/// A source change releases its policy only after applying the old constraints.
|
||||
mutating func save(
|
||||
_ settings: SubscriptionAutoUpdateSettings?, sourceChanged: Bool = false,
|
||||
at date: Date, jitter: Double
|
||||
) {
|
||||
let previousMode = effectiveMode
|
||||
if let settings, allowsUserOverride {
|
||||
userOverride = settings.userOverride
|
||||
interval = settings.interval == .weekly ? .weekly : .daily
|
||||
}
|
||||
if sourceChanged { provider = nil }
|
||||
if previousMode != effectiveMode { schedule(from: date, jitter: jitter) }
|
||||
}
|
||||
|
||||
mutating func refreshSucceeded(
|
||||
policy: SubscriptionUpdatePolicyResult, at date: Date, jitter: Double
|
||||
) {
|
||||
switch policy {
|
||||
case .absent: provider = nil
|
||||
case .valid(let policy): provider = policy
|
||||
case .invalid: break
|
||||
}
|
||||
schedule(from: date, jitter: jitter)
|
||||
}
|
||||
|
||||
mutating func schedule(from date: Date, jitter: Double) {
|
||||
mutating func refreshFailed(at date: Date, jitter: Double) {
|
||||
schedule(from: date, jitter: jitter)
|
||||
}
|
||||
|
||||
private mutating func schedule(from date: Date, jitter: Double) {
|
||||
nextUpdateAt = effectiveMode.interval.map {
|
||||
date.addingTimeInterval($0 * (1 + min(max(jitter, 0), 0.1)))
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ struct CatalogWorkflowLibraryTests {
|
||||
try await requireTestCondition("old URL fetch entered") { fetcher.didEnterFetch }
|
||||
let edit = Task {
|
||||
do {
|
||||
try await workflow.editSubscriptionURL(record.id, urlString: "https://new.example/sub")
|
||||
try await workflow.saveSubscription(record.id, urlString: "https://new.example/sub")
|
||||
} catch { Issue.record(error) }
|
||||
}
|
||||
try await withTestTask(edit, release: fetcher.releaseAll) {
|
||||
@@ -184,8 +184,11 @@ struct CatalogWorkflowLibraryTests {
|
||||
try FileManager.default.removeItem(at: gate)
|
||||
try Data().write(to: gate)
|
||||
await #expect(throws: (any Error).self) {
|
||||
try await workflow.editSubscriptionURL(subscription.id, urlString: "https://new.example/sub")
|
||||
try await workflow.saveSubscription(
|
||||
subscription.id, urlString: "https://new.example/sub",
|
||||
autoUpdate: .init(userOverride: .daily))
|
||||
}
|
||||
#expect(workflow.subscriptions.first?.autoUpdate == subscription.autoUpdate)
|
||||
#expect(try workflow.subscriptionURL(for: subscription.id) == "https://old.example/sub")
|
||||
#expect(fetcher.requestCount == requests)
|
||||
#expect(runtime.convergeCount == 0)
|
||||
|
||||
@@ -160,7 +160,7 @@ final class CatalogWorkflowSubscriptionTests: XCTestCase {
|
||||
|
||||
// 新地址先失败:快照保留、身份不动。
|
||||
fetcher.setBehavior(.failure(.httpStatus(code: 404)))
|
||||
try await workflow.editSubscriptionURL(
|
||||
try await workflow.saveSubscription(
|
||||
record.id, urlString: "https://other.example.com/v2.json")
|
||||
XCTAssertEqual(workflow.subscriptions[0].id, originalRecordID, "订阅身份保留")
|
||||
guard case .failed = workflow.subscriptions[0].status else {
|
||||
@@ -182,7 +182,7 @@ final class CatalogWorkflowSubscriptionTests: XCTestCase {
|
||||
func testEditURLRequiresHTTPS() async throws {
|
||||
let record = try await seedSuccessfulSubscription()
|
||||
await expectThrowsAsync(
|
||||
{ try await workflow.editSubscriptionURL(record.id, urlString: "http://p.example.com/x") },
|
||||
{ try await workflow.saveSubscription(record.id, urlString: "http://p.example.com/x") },
|
||||
onThrow: { error in
|
||||
XCTAssertEqual(error as? SubscriptionFormError, .invalidURL)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@testable import ShadowsocksX_NG2
|
||||
|
||||
struct SubscriptionAutoUpdateTests {
|
||||
@Test func savingSettingsPreservesPlanUntilEffectiveModeChanges() {
|
||||
let now = Date(timeIntervalSince1970: 1_700_000_000)
|
||||
var state = SubscriptionAutoUpdate()
|
||||
state.save(.init(userOverride: .daily, interval: .off), at: now, jitter: 0.1)
|
||||
#expect(state.interval == .daily)
|
||||
#expect(state.nextUpdateAt == now.addingTimeInterval(95_040))
|
||||
state.save(.init(userOverride: .daily, interval: .weekly), at: now, jitter: 0)
|
||||
#expect(state.interval == .weekly)
|
||||
#expect(state.nextUpdateAt == now.addingTimeInterval(95_040))
|
||||
state.save(.init(userOverride: .off, interval: .weekly), at: now, jitter: 0)
|
||||
#expect(state.nextUpdateAt == nil)
|
||||
#expect(state.interval == .weekly)
|
||||
}
|
||||
|
||||
@Test func changingSourceReleasesPolicyAndRetainsTheLockedUserChoice() {
|
||||
let now = Date(timeIntervalSince1970: 1_700_000_000)
|
||||
var state = SubscriptionAutoUpdate()
|
||||
state.save(.init(userOverride: .weekly, interval: .weekly), at: now, jitter: 0)
|
||||
state.refreshSucceeded(
|
||||
policy: .valid(.init(mode: .daily, allowUserOverride: false)), at: now, jitter: 0)
|
||||
state.save(
|
||||
.init(userOverride: .off), sourceChanged: true,
|
||||
at: now.addingTimeInterval(100), jitter: 0)
|
||||
#expect(state.provider == nil)
|
||||
#expect(state.userOverride == .weekly)
|
||||
#expect(state.interval == .weekly)
|
||||
#expect(state.nextUpdateAt == now.addingTimeInterval(604_900))
|
||||
}
|
||||
|
||||
@Test func refreshResultsDistinguishWithdrawalFromInvalidPolicyAndFailure() {
|
||||
let now = Date(timeIntervalSince1970: 1_700_000_000)
|
||||
let policy = SubscriptionUpdatePolicy(mode: .daily, allowUserOverride: false)
|
||||
var state = SubscriptionAutoUpdate()
|
||||
state.refreshSucceeded(policy: .valid(policy), at: now, jitter: 0)
|
||||
#expect(state.nextUpdateAt == now.addingTimeInterval(86_400))
|
||||
state.refreshSucceeded(policy: .invalid, at: now.addingTimeInterval(100), jitter: 0.1)
|
||||
#expect(state.provider == policy)
|
||||
#expect(state.nextUpdateAt == now.addingTimeInterval(95_140))
|
||||
state.refreshFailed(at: now.addingTimeInterval(200), jitter: 0)
|
||||
#expect(state.provider == policy)
|
||||
#expect(state.nextUpdateAt == now.addingTimeInterval(86_600))
|
||||
state.refreshSucceeded(policy: .absent, at: now.addingTimeInterval(300), jitter: 0)
|
||||
#expect(state.provider == nil)
|
||||
#expect(state.nextUpdateAt == nil)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,7 +15,8 @@ extension SubscriptionAutoUpdateWorkflowTests {
|
||||
await workflow.refreshSubscription(first.id)
|
||||
#expect(fetcher.requestCount == 1)
|
||||
fixture.now = fixture.now.addingTimeInterval(100)
|
||||
try workflow.saveSubscriptionAutoUpdate(first.id, settings: .init(userOverride: .weekly))
|
||||
try await workflow.saveSubscription(
|
||||
first.id, urlString: "https://example.com/sub", autoUpdate: .init(userOverride: .weekly))
|
||||
fixture.now = fixture.now.addingTimeInterval(200)
|
||||
}
|
||||
#expect(
|
||||
@@ -44,7 +45,7 @@ extension SubscriptionAutoUpdateWorkflowTests {
|
||||
var current: [SubscriptionSummary] = []
|
||||
try await withTestTask(request, release: fetcher.release.release) {
|
||||
try #require(await fetcher.entered.wait())
|
||||
try await workflow.editSubscriptionURL(first.id, urlString: "https://new.example.com/sub")
|
||||
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)
|
||||
|
||||
@@ -31,7 +31,8 @@ struct SubscriptionAutoUpdateWorkflowTests {
|
||||
try await withAutoUpdateFixture { fixture in
|
||||
fixture.jitter = jitter
|
||||
let first = try await fixture.create()
|
||||
try fixture.workflow.saveSubscriptionAutoUpdate(first.id, settings: .init(userOverride: mode))
|
||||
try await fixture.workflow.saveSubscription(
|
||||
first.id, urlString: "https://example.com/sub", autoUpdate: .init(userOverride: mode))
|
||||
let seconds: TimeInterval =
|
||||
mode == .daily ? (jitter == 0 ? 86_400 : 95_040) : (jitter == 0 ? 604_800 : 665_280)
|
||||
#expect(fixture.summary.autoUpdate.nextUpdateAt == fixture.now.addingTimeInterval(seconds))
|
||||
@@ -52,19 +53,21 @@ struct SubscriptionAutoUpdateWorkflowTests {
|
||||
@Test func forcedPolicyPreservesOverrideThenFollowingClearsIt() async throws {
|
||||
try await withAutoUpdateFixture { fixture in
|
||||
let first = try await fixture.create()
|
||||
try fixture.workflow.saveSubscriptionAutoUpdate(
|
||||
first.id, settings: .init(userOverride: .weekly, interval: .weekly))
|
||||
try await fixture.workflow.saveSubscription(
|
||||
first.id, urlString: "https://example.com/sub",
|
||||
autoUpdate: .init(userOverride: .weekly, interval: .weekly))
|
||||
fixture.respond(mode: .off, allowsOverride: false)
|
||||
await fixture.workflow.refreshSubscription(first.id)
|
||||
#expect(fixture.summary.autoUpdate.effectiveMode == .off)
|
||||
#expect(fixture.summary.autoUpdate.userOverride == .weekly)
|
||||
try fixture.workflow.saveSubscriptionAutoUpdate(
|
||||
first.id, settings: .init(userOverride: .daily))
|
||||
try await fixture.workflow.saveSubscription(
|
||||
first.id, urlString: "https://example.com/sub", autoUpdate: .init(userOverride: .daily))
|
||||
#expect(fixture.summary.autoUpdate.userOverride == .weekly)
|
||||
fixture.respond(mode: .daily, allowsOverride: true)
|
||||
await fixture.workflow.refreshSubscription(first.id)
|
||||
#expect(fixture.summary.autoUpdate.effectiveMode == .weekly)
|
||||
try fixture.workflow.saveSubscriptionAutoUpdate(first.id, settings: .init())
|
||||
try await fixture.workflow.saveSubscription(
|
||||
first.id, urlString: "https://example.com/sub", autoUpdate: .init())
|
||||
#expect(fixture.summary.autoUpdate.userOverride == nil)
|
||||
#expect(fixture.summary.autoUpdate.effectiveMode == .daily)
|
||||
fixture.fetcher.setBehavior(.success(SubscriptionDocs.flat(serverCount: 0)))
|
||||
@@ -112,14 +115,16 @@ struct SubscriptionAutoUpdateWorkflowTests {
|
||||
let plan = fixture.summary.autoUpdate.nextUpdateAt
|
||||
let requests = fixture.fetcher.requestCount
|
||||
fixture.now = fixture.now.addingTimeInterval(100)
|
||||
try fixture.workflow.saveSubscriptionAutoUpdate(
|
||||
first.id, settings: .init(userOverride: .daily))
|
||||
try await fixture.workflow.saveSubscription(
|
||||
first.id, urlString: "https://example.com/sub", autoUpdate: .init(userOverride: .daily))
|
||||
#expect(fixture.summary.autoUpdate.nextUpdateAt == plan)
|
||||
try fixture.workflow.saveSubscriptionAutoUpdate(
|
||||
first.id, settings: .init(userOverride: .weekly, interval: .weekly))
|
||||
try await fixture.workflow.saveSubscription(
|
||||
first.id, urlString: "https://example.com/sub",
|
||||
autoUpdate: .init(userOverride: .weekly, interval: .weekly))
|
||||
#expect(fixture.summary.autoUpdate.nextUpdateAt == fixture.now.addingTimeInterval(604_800))
|
||||
try fixture.workflow.saveSubscriptionAutoUpdate(
|
||||
first.id, settings: .init(userOverride: .off, interval: .weekly))
|
||||
try await fixture.workflow.saveSubscription(
|
||||
first.id, urlString: "https://example.com/sub",
|
||||
autoUpdate: .init(userOverride: .off, interval: .weekly))
|
||||
#expect(fixture.summary.autoUpdate.nextUpdateAt == nil)
|
||||
#expect(fixture.summary.autoUpdate.interval == .weekly)
|
||||
#expect(fixture.fetcher.requestCount == requests)
|
||||
@@ -206,7 +211,9 @@ final class AutoUpdateFixture {
|
||||
subscriptionJitter: { [unowned self] in self.jitter }, subscriptionWait: wait)
|
||||
}
|
||||
|
||||
func create(settings: SubscriptionAutoUpdate = .init()) async throws -> SubscriptionSummary {
|
||||
func create(settings: SubscriptionAutoUpdateSettings = .init()) async throws
|
||||
-> SubscriptionSummary
|
||||
{
|
||||
try await workflow.createSubscription(
|
||||
urlString: "https://example.com/sub", autoUpdate: settings)
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ struct SubscriptionRefreshFeedbackTests {
|
||||
let failure = try #require(fixture.workflow.subscriptionRefreshCompletions[summary.id])
|
||||
#expect(failure.failure == .httpStatus(code: 503))
|
||||
fixture.fetcher.setBehavior(.success(SubscriptionDocs.flat(serverCount: 1)))
|
||||
try await fixture.workflow.editSubscriptionURL(
|
||||
try await fixture.workflow.saveSubscription(
|
||||
summary.id, urlString: "https://example.com/new.json")
|
||||
let success = try #require(fixture.workflow.subscriptionRefreshCompletions[summary.id])
|
||||
#expect(success.failure == nil)
|
||||
|
||||
Reference in New Issue
Block a user