mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 02:29:55 +02:00
test(apple): CoreGateway seam, XCTest suite, and CI
- Introduce a CoreGateway protocol so feature models depend on a seam (CoreRepository conforms); enables faking the core in tests - Add a VniDropTests target with 42 tests mirroring the KMP suites: approval coordinator, send/receive/settings/app models, preferences, file previews, invitation decode, message queue, error mapping - Add a fake gateway/file-system/device-info and fixtures - Add .github/workflows/apple.yml: build the Rust core, generate the project, and run the tests on an iOS Simulator
This commit is contained in:
38
apple/VniDrop/Core/CoreGateway.swift
Normal file
38
apple/VniDrop/Core/CoreGateway.swift
Normal file
@@ -0,0 +1,38 @@
|
||||
import Foundation
|
||||
import Combine
|
||||
import VnidropCore
|
||||
|
||||
/// Seam between the feature models and the Rust core, mirroring `CoreGateway`
|
||||
/// in the KMP `shared` module. `CoreRepository` is the production implementation;
|
||||
/// tests substitute a fake so the models can be exercised without the FFI.
|
||||
@MainActor
|
||||
protocol CoreGateway: AnyObject {
|
||||
/// Latest published core state.
|
||||
var state: CoreState { get }
|
||||
/// Publisher of core-state changes (the models subscribe to this).
|
||||
var statePublisher: AnyPublisher<CoreState, Never> { get }
|
||||
/// Coalesced change hints emitted by the event sink.
|
||||
var signals: AnyPublisher<CoreSignal, Never> { get }
|
||||
|
||||
func initialize(appDataDir: String) async -> Result<Void, Error>
|
||||
func shutdown()
|
||||
func shareSources(
|
||||
_ sources: [ShareSource],
|
||||
transferName: String,
|
||||
senderName: String,
|
||||
accessPolicy: ShareAccessPolicy
|
||||
) async -> Result<Share, Error>
|
||||
func inspectTicket(_ ticket: String) async -> Result<TicketInspectionModel, Error>
|
||||
func receive(ticket: String, outputDir: String, receiverName: String) async -> Result<Void, Error>
|
||||
func receiveIntoSecurityScopedDirectory(
|
||||
ticket: String,
|
||||
outputDirectoryUrl: String,
|
||||
receiverName: String
|
||||
) async -> Result<Void, Error>
|
||||
func cancel(transferId: UInt64) async -> Result<Void, Error>
|
||||
func delete(transferId: UInt64) async -> Result<Void, Error>
|
||||
func clearReceiveHistory() async -> Result<UInt64, Error>
|
||||
func receiverRequests(transferId: UInt64) async -> Result<[ReceiverRequestModel], Error>
|
||||
func respondReceiverRequest(requestId: String, accepted: Bool, reason: String?) async -> Result<Void, Error>
|
||||
func refresh() async -> Result<Void, Error>
|
||||
}
|
||||
@@ -9,8 +9,9 @@ import Combine
|
||||
/// UniFFI calls block (the core drives its own runtime via `block_on`), so they
|
||||
/// run on a background queue and results are hopped back to the main actor.
|
||||
@MainActor
|
||||
final class CoreRepository: ObservableObject {
|
||||
final class CoreRepository: ObservableObject, CoreGateway {
|
||||
@Published private(set) var state = CoreState()
|
||||
var statePublisher: AnyPublisher<CoreState, Never> { $state.eraseToAnyPublisher() }
|
||||
|
||||
private let signalsSubject = PassthroughSubject<CoreSignal, Never>()
|
||||
/// Coalesced change hints; subscribe to react to approval/history/transfer changes.
|
||||
|
||||
@@ -30,7 +30,7 @@ protocol FileSystemService {
|
||||
/// Releases only app-owned picker copies; never deletes original user sources.
|
||||
func discardPickedFiles(_ files: [PickedShareFile]) async
|
||||
func sharePickedFiles(
|
||||
repository: CoreRepository,
|
||||
repository: CoreGateway,
|
||||
files: [PickedShareFile],
|
||||
transferName: String,
|
||||
senderName: String,
|
||||
|
||||
@@ -9,13 +9,13 @@ final class AppModel: ObservableObject {
|
||||
@Published private(set) var themeMode: ThemeMode = .system
|
||||
|
||||
private let environment: PlatformEnvironment
|
||||
private let repository: CoreRepository
|
||||
private let repository: CoreGateway
|
||||
private let messages: UiMessageController
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(
|
||||
environment: PlatformEnvironment,
|
||||
repository: CoreRepository,
|
||||
repository: CoreGateway,
|
||||
preferences: AppPreferencesRepository,
|
||||
messages: UiMessageController
|
||||
) {
|
||||
|
||||
@@ -26,7 +26,7 @@ struct ApprovalState: Equatable {
|
||||
final class ApprovalCoordinator: ObservableObject {
|
||||
@Published private(set) var state = ApprovalState()
|
||||
|
||||
private let repository: CoreRepository
|
||||
private let repository: CoreGateway
|
||||
private let preferences: AppPreferencesRepository
|
||||
private let notifications: LocalNotificationService
|
||||
private let visibility: AppVisibility
|
||||
@@ -36,7 +36,7 @@ final class ApprovalCoordinator: ObservableObject {
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(
|
||||
repository: CoreRepository,
|
||||
repository: CoreGateway,
|
||||
preferences: AppPreferencesRepository,
|
||||
notifications: LocalNotificationService,
|
||||
visibility: AppVisibility,
|
||||
@@ -57,7 +57,7 @@ final class ApprovalCoordinator: ObservableObject {
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
repository.$state
|
||||
repository.statePublisher
|
||||
.sink { [weak self] core in
|
||||
guard let self, core.isInitialized else { return }
|
||||
let sharing = core.transfers.filter { $0.direction == .send && $0.status == .sharing }
|
||||
|
||||
@@ -51,13 +51,13 @@ final class ReceiveModel: ObservableObject {
|
||||
@Published private(set) var state = ReceiveState()
|
||||
@Published private(set) var coreState = CoreState()
|
||||
|
||||
private let repository: CoreRepository
|
||||
private let repository: CoreGateway
|
||||
private let fileSystemService: FileSystemService
|
||||
private let messages: UiMessageController
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(
|
||||
repository: CoreRepository,
|
||||
repository: CoreGateway,
|
||||
fileSystemService: FileSystemService,
|
||||
preferences: AppPreferencesRepository,
|
||||
messages: UiMessageController
|
||||
@@ -66,7 +66,7 @@ final class ReceiveModel: ObservableObject {
|
||||
self.fileSystemService = fileSystemService
|
||||
self.messages = messages
|
||||
|
||||
repository.$state.sink { [weak self] in self?.coreState = $0 }.store(in: &cancellables)
|
||||
repository.statePublisher.sink { [weak self] in self?.coreState = $0 }.store(in: &cancellables)
|
||||
|
||||
preferences.$preferences
|
||||
.sink { [weak self] prefs in
|
||||
|
||||
@@ -50,14 +50,14 @@ final class SendModel: ObservableObject {
|
||||
/// small transfer complete.
|
||||
@Published private(set) var receiversByTransfer: [UInt64: [ReceiverRequestModel]] = [:]
|
||||
|
||||
private let repository: CoreRepository
|
||||
private let repository: CoreGateway
|
||||
private let fileSystemService: FileSystemService
|
||||
private let filePreviewRepository: FilePreviewRepository
|
||||
private let messages: UiMessageController
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(
|
||||
repository: CoreRepository,
|
||||
repository: CoreGateway,
|
||||
fileSystemService: FileSystemService,
|
||||
preferences: AppPreferencesRepository,
|
||||
filePreviewRepository: FilePreviewRepository,
|
||||
@@ -68,7 +68,7 @@ final class SendModel: ObservableObject {
|
||||
self.filePreviewRepository = filePreviewRepository
|
||||
self.messages = messages
|
||||
|
||||
repository.$state.sink { [weak self] in self?.coreState = $0 }.store(in: &cancellables)
|
||||
repository.statePublisher.sink { [weak self] in self?.coreState = $0 }.store(in: &cancellables)
|
||||
|
||||
repository.signals
|
||||
.sink { [weak self] signal in
|
||||
@@ -86,7 +86,7 @@ final class SendModel: ObservableObject {
|
||||
// Keep receiver delivery records current for every sharing/importing
|
||||
// outgoing transfer (new shares appear here; status transitions arrive via
|
||||
// the receiverHistoryChanged signal above).
|
||||
repository.$state
|
||||
repository.statePublisher
|
||||
.map { core -> Set<UInt64> in
|
||||
Set(core.transfers
|
||||
.filter { $0.direction == .send && ($0.status == .sharing || $0.status == .importing) }
|
||||
@@ -100,7 +100,7 @@ final class SendModel: ObservableObject {
|
||||
.sink { [weak self] previews in self?.state.transferThumbnails = previews }
|
||||
.store(in: &cancellables)
|
||||
|
||||
repository.$state
|
||||
repository.statePublisher
|
||||
.map { core -> Set<UInt64>? in
|
||||
core.isInitialized ? Set(core.transfers.map(\.transferId)) : nil
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ final class SettingsModel: ObservableObject {
|
||||
private let environment: PlatformEnvironment
|
||||
private let deviceInfoProvider: DeviceInfoProvider
|
||||
private let fileSystemService: FileSystemService
|
||||
private let repository: CoreRepository
|
||||
private let repository: CoreGateway
|
||||
private let preferences: AppPreferencesRepository
|
||||
private let notifications: LocalNotificationService
|
||||
private let messages: UiMessageController
|
||||
@@ -101,7 +101,7 @@ final class SettingsModel: ObservableObject {
|
||||
environment: PlatformEnvironment,
|
||||
deviceInfoProvider: DeviceInfoProvider,
|
||||
fileSystemService: FileSystemService,
|
||||
repository: CoreRepository,
|
||||
repository: CoreGateway,
|
||||
preferences: AppPreferencesRepository,
|
||||
notifications: LocalNotificationService,
|
||||
messages: UiMessageController,
|
||||
|
||||
@@ -55,7 +55,7 @@ struct IosFileSystemService: FileSystemService {
|
||||
}
|
||||
|
||||
func sharePickedFiles(
|
||||
repository: CoreRepository,
|
||||
repository: CoreGateway,
|
||||
files: [PickedShareFile],
|
||||
transferName: String,
|
||||
senderName: String,
|
||||
|
||||
@@ -35,7 +35,7 @@ struct MacFileSystemService: FileSystemService {
|
||||
}
|
||||
|
||||
func sharePickedFiles(
|
||||
repository: CoreRepository,
|
||||
repository: CoreGateway,
|
||||
files: [PickedShareFile],
|
||||
transferName: String,
|
||||
senderName: String,
|
||||
|
||||
Reference in New Issue
Block a user