Files

208 lines
9.7 KiB
Swift

import Foundation
import Testing
@testable import ShadowsocksX_NG2
@MainActor
struct CatalogWorkflowLibraryTests {
@Test func failedLoadPreventsEditsAndPreservesFile() async throws {
let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
defer { try? FileManager.default.removeItem(at: directory) }
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
let url = directory.appendingPathComponent("catalog.json")
let damaged = Data("damaged-library".utf8)
try damaged.write(to: url)
let runtime = FakeCatalogRuntime()
runtime.hasActiveTarget = true
let coordinator = CatalogCommitCoordinator(
fileStore: makeTestCatalogStore(fileURL: url), runtime: runtime)
let workflow = makeCatalogWorkflow(
coordinator: coordinator)
await #expect(throws: (any Error).self) {
try await workflow.createGroup(named: "must-not-save", into: nil)
}
#expect(try Data(contentsOf: url) == damaged)
#expect(runtime.convergeCount == 0)
}
@Test func explicitUnlockLoadsLibraryAndResumesRuntime() async throws {
let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
defer { try? FileManager.default.removeItem(at: directory) }
let url = directory.appendingPathComponent("catalog.age")
let keys = TestLibraryKeys()
func makeStore() -> CatalogFileStore {
CatalogFileStore(
fileURL: url,
library: ServerLibraryStorage(
keys: keys))
}
let original = makeStore()
_ = try original.load()
var catalog = ConfigurationCatalog()
_ = try catalog.addGroup("saved-group")
try original.save(CatalogDocument(catalog: catalog))
let encrypted = try Data(contentsOf: url)
keys.requiresInteraction = true
let store = makeStore()
let runtime = FakeCatalogRuntime()
let coordinator = CatalogCommitCoordinator(fileStore: store, runtime: runtime)
let workflow = makeCatalogWorkflow(coordinator: coordinator)
#expect(workflow.libraryFailure == .authorization(-25308))
#expect(!coordinator.committedCatalog.contains(catalog.rootChildren[0]))
#expect(runtime.convergeCount == 0)
await withCheckedContinuation { continuation in
runtime.onNextConverge = { continuation.resume() }
workflow.unlockLibrary()
}
#expect(workflow.libraryFailure == nil)
#expect(workflow.tree.roots.first?.name == "saved-group")
#expect(try Data(contentsOf: url) == encrypted)
#expect(runtime.convergeCount == 1)
}
@Test func failedResetPreservesOldLibraryAndSuccessfulResetUsesNewKey() async throws {
let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
defer { try? FileManager.default.removeItem(at: directory) }
let url = directory.appendingPathComponent("catalog.age")
let keys = TestLibraryKeys()
let store = CatalogFileStore(
fileURL: url,
library: ServerLibraryStorage(
keys: keys))
_ = try store.load()
var catalog = ConfigurationCatalog()
_ = try catalog.addGroup("saved-group")
try store.save(CatalogDocument(catalog: catalog))
let encrypted = try Data(contentsOf: url)
let runtime = FakeCatalogRuntime()
runtime.hasActiveTarget = true
let coordinator = CatalogCommitCoordinator(fileStore: store, runtime: runtime)
let workflow = makeCatalogWorkflow(coordinator: coordinator)
keys.createFails = true
workflow.resetLibrary()
#expect(workflow.libraryFailure != nil)
#expect(try Data(contentsOf: url) == encrypted)
#expect(workflow.tree.roots.first?.name == "saved-group")
#expect(runtime.convergeCount == 0)
keys.createFails = false
await withCheckedContinuation { continuation in
runtime.onNextConverge = { continuation.resume() }
workflow.resetLibrary()
}
#expect(workflow.libraryFailure == nil)
#expect(workflow.tree.roots.isEmpty)
#expect(try store.load().catalog.isEmpty)
#expect(keys.values.count == 2)
#expect(runtime.convergeSnapshots.last?.catalog.isEmpty == true)
}
@Test func failedLoadDoesNotClearActiveTargetOrRewriteRuntimeOnLaunch() async throws {
let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
defer { try? FileManager.default.removeItem(at: directory) }
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
let url = directory.appendingPathComponent("catalog.json")
try Data("broken-library".utf8).write(to: url)
let activation = ActivationStateFileStore(
fileURL: directory.appendingPathComponent("activation.json"))
let target = NodeID(rawValue: "existing-target")
try activation.save(activeTargetID: target)
let runtimeURL = directory.appendingPathComponent("sslocal-active.json")
let previousContract = Data("existing-runtime-contract".utf8)
try previousContract.write(to: runtimeURL)
let bootstrap = CatalogCommitCoordinator.bootstrap(
fileStore: makeTestCatalogStore(fileURL: url))
let agent = ProxyRuntimeFixture.FakeLaunchAgent(initialStatus: .registered)
let controller = ProxyRuntimeController(
controlPreferences: makeTestProxyControlPreferences(),
catalogSnapshotReader: bootstrap.catalogSnapshotReader, activationFileStore: activation,
runtimeFileStore: RuntimeFileStore(fileURL: runtimeURL),
settingsRestore: RestoredProxyRuntimeSettings(
settings: ProxyRuntimeSettings(agentEnabled: true), unreadableError: nil),
agent: agent)
await controller.resyncOnLaunch()
await controller.convergeAgent()
#expect(try activation.loadActiveTargetID() == target)
#expect(controller.activeTargetID == target)
#expect(try Data(contentsOf: runtimeURL) == previousContract)
#expect(agent.registerCount == 0)
#expect(agent.unregisterCount == 0)
}
@Test func editedSubscriptionURLPersistsWhileRefreshIsInProgress() async throws {
let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
defer { try? FileManager.default.removeItem(at: directory) }
let url = directory.appendingPathComponent("catalog.age")
let keys = TestLibraryKeys()
let fixture = try CatalogFixtures.makeSubscriptionFixture(prefix: "edit-url")
let record = SubscriptionRecord(
id: NodeID.fresh(), groupID: fixture.groupID,
url: "https://old.example/sub", status: .never)
let document = CatalogDocument(catalog: fixture.catalog, subscriptions: [record])
let store = CatalogFileStore(
fileURL: url,
library: ServerLibraryStorage(
keys: keys))
_ = try store.load()
try store.save(document)
let fetcher = GatedFetcher()
let workflow = makeCatalogWorkflow(
coordinator: CatalogCommitCoordinator(fileStore: store, runtime: FakeCatalogRuntime()),
subscriptionFetcher: fetcher)
let refresh = Task { await workflow.refreshSubscription(record.id) }
try await withTestTask(refresh, release: fetcher.releaseAll) {
try await requireTestCondition("old URL fetch entered") { fetcher.didEnterFetch }
let edit = Task {
do {
try await workflow.saveSubscription(record.id, urlString: "https://new.example/sub")
} catch { Issue.record(error) }
}
try await withTestTask(edit, release: fetcher.releaseAll) {
try await requireTestCondition("new URL fetch entered") { fetcher.requestCount == 2 }
let restored = CatalogFileStore(
fileURL: url, library: ServerLibraryStorage(keys: keys))
_ = try restored.load()
#expect(try restored.load().subscriptions.first?.url == "https://new.example/sub")
}
}
}
@Test func failedURLSaveDoesNotFetchOrPublishAndFailedRefreshKeepsSecrets() async throws {
let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
defer { try? FileManager.default.removeItem(at: directory) }
let store = makeTestCatalogStore(fileURL: directory.appendingPathComponent("gate/catalog.age"))
let fetcher = FakeSubscriptionFetcher(behavior: .success(SubscriptionDocs.tree()))
let runtime = FakeCatalogRuntime()
let workflow = makeCatalogWorkflow(
coordinator: CatalogCommitCoordinator(fileStore: store, runtime: runtime),
subscriptionFetcher: fetcher)
let subscription = try await workflow.createSubscription(urlString: "https://old.example/sub")
let server = try #require(
workflow.tree.node(withID: subscription.groupID)?.childNodes
.first(where: { !$0.isGroup }))
let before = try #require(try workflow.serverEditForm(for: server.id))
let requests = fetcher.requestCount
let gate = directory.appendingPathComponent("gate")
try FileManager.default.removeItem(at: gate)
try Data().write(to: gate)
await #expect(throws: (any Error).self) {
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)
let updatedDocument = try #require(String(bytes: SubscriptionDocs.tree(), encoding: .utf8))
fetcher.setBehavior(
.success(
Data(
updatedDocument.replacingOccurrences(of: "\"pa\"", with: "\"changed-password\"").utf8)))
await workflow.refreshSubscription(subscription.id)
let after = try #require(try workflow.serverEditForm(for: server.id))
#expect(after.password == before.password)
#expect(after.plugin.options == before.plugin.options)
#expect(workflow.subscriptionRefreshFailure(for: subscription.id) == .commit)
}
}