From f3ba330cf40e85605684035e9a79d58a49016bcd Mon Sep 17 00:00:00 2001 From: Qiu Yuzhou Date: Sun, 25 Oct 2026 22:06:39 +0800 Subject: [PATCH] refactor: centralize subscription automatic update transitions --- .../CatalogWorkflow+Subscriptions.swift | 57 +++++-------------- .../App/UI/Servers/ServersView.swift | 2 +- .../UI/Subscriptions/SubscriptionsView.swift | 25 ++++---- .../Subscription/SubscriptionAutoUpdate.swift | 45 ++++++++++++--- .../CatalogWorkflowLibraryTests.swift | 7 ++- .../CatalogWorkflowSubscriptionTests.swift | 4 +- .../SubscriptionAutoUpdateTests.swift | 53 +++++++++++++++++ ...riptionAutoUpdateWorkflowTests+Races.swift | 5 +- .../SubscriptionAutoUpdateWorkflowTests.swift | 33 ++++++----- .../SubscriptionRefreshFeedbackTests.swift | 2 +- 10 files changed, 151 insertions(+), 82 deletions(-) create mode 100644 ShadowsocksX-NG2/Tests/Subscription/SubscriptionAutoUpdateTests.swift diff --git a/ShadowsocksX-NG2/App/Application/CatalogWorkflow/CatalogWorkflow+Subscriptions.swift b/ShadowsocksX-NG2/App/Application/CatalogWorkflow/CatalogWorkflow+Subscriptions.swift index c83adcd0..9e8ef0c5 100644 --- a/ShadowsocksX-NG2/App/Application/CatalogWorkflow/CatalogWorkflow+Subscriptions.swift +++ b/ShadowsocksX-NG2/App/Application/CatalogWorkflow/CatalogWorkflow+Subscriptions.swift @@ -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()) } } } diff --git a/ShadowsocksX-NG2/App/UI/Servers/ServersView.swift b/ShadowsocksX-NG2/App/UI/Servers/ServersView.swift index 4e7d7fed..49543368 100644 --- a/ShadowsocksX-NG2/App/UI/Servers/ServersView.swift +++ b/ShadowsocksX-NG2/App/UI/Servers/ServersView.swift @@ -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) diff --git a/ShadowsocksX-NG2/App/UI/Subscriptions/SubscriptionsView.swift b/ShadowsocksX-NG2/App/UI/Subscriptions/SubscriptionsView.swift index a85e35a0..1b050218 100644 --- a/ShadowsocksX-NG2/App/UI/Subscriptions/SubscriptionsView.swift +++ b/ShadowsocksX-NG2/App/UI/Subscriptions/SubscriptionsView.swift @@ -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) } diff --git a/ShadowsocksX-NG2/Domain/Subscription/SubscriptionAutoUpdate.swift b/ShadowsocksX-NG2/Domain/Subscription/SubscriptionAutoUpdate.swift index 38dbbe8d..de791b29 100644 --- a/ShadowsocksX-NG2/Domain/Subscription/SubscriptionAutoUpdate.swift +++ b/ShadowsocksX-NG2/Domain/Subscription/SubscriptionAutoUpdate.swift @@ -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))) } diff --git a/ShadowsocksX-NG2/Tests/Catalog/Workflow/CatalogWorkflowLibraryTests.swift b/ShadowsocksX-NG2/Tests/Catalog/Workflow/CatalogWorkflowLibraryTests.swift index 9299c58b..e6b6923c 100644 --- a/ShadowsocksX-NG2/Tests/Catalog/Workflow/CatalogWorkflowLibraryTests.swift +++ b/ShadowsocksX-NG2/Tests/Catalog/Workflow/CatalogWorkflowLibraryTests.swift @@ -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) diff --git a/ShadowsocksX-NG2/Tests/Catalog/Workflow/CatalogWorkflowSubscriptionTests.swift b/ShadowsocksX-NG2/Tests/Catalog/Workflow/CatalogWorkflowSubscriptionTests.swift index b2c9d0e2..c608d2a0 100644 --- a/ShadowsocksX-NG2/Tests/Catalog/Workflow/CatalogWorkflowSubscriptionTests.swift +++ b/ShadowsocksX-NG2/Tests/Catalog/Workflow/CatalogWorkflowSubscriptionTests.swift @@ -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) }) diff --git a/ShadowsocksX-NG2/Tests/Subscription/SubscriptionAutoUpdateTests.swift b/ShadowsocksX-NG2/Tests/Subscription/SubscriptionAutoUpdateTests.swift new file mode 100644 index 00000000..d8b35a8d --- /dev/null +++ b/ShadowsocksX-NG2/Tests/Subscription/SubscriptionAutoUpdateTests.swift @@ -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) + } + +} diff --git a/ShadowsocksX-NG2/Tests/Subscription/SubscriptionAutoUpdateWorkflowTests+Races.swift b/ShadowsocksX-NG2/Tests/Subscription/SubscriptionAutoUpdateWorkflowTests+Races.swift index acabcfd6..0464a96e 100644 --- a/ShadowsocksX-NG2/Tests/Subscription/SubscriptionAutoUpdateWorkflowTests+Races.swift +++ b/ShadowsocksX-NG2/Tests/Subscription/SubscriptionAutoUpdateWorkflowTests+Races.swift @@ -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) diff --git a/ShadowsocksX-NG2/Tests/Subscription/SubscriptionAutoUpdateWorkflowTests.swift b/ShadowsocksX-NG2/Tests/Subscription/SubscriptionAutoUpdateWorkflowTests.swift index bd4ce5bc..47d974a7 100644 --- a/ShadowsocksX-NG2/Tests/Subscription/SubscriptionAutoUpdateWorkflowTests.swift +++ b/ShadowsocksX-NG2/Tests/Subscription/SubscriptionAutoUpdateWorkflowTests.swift @@ -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) } diff --git a/ShadowsocksX-NG2/Tests/Subscription/SubscriptionRefreshFeedbackTests.swift b/ShadowsocksX-NG2/Tests/Subscription/SubscriptionRefreshFeedbackTests.swift index 81c6e021..3377afe0 100644 --- a/ShadowsocksX-NG2/Tests/Subscription/SubscriptionRefreshFeedbackTests.swift +++ b/ShadowsocksX-NG2/Tests/Subscription/SubscriptionRefreshFeedbackTests.swift @@ -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)