Implement remote library metadata sync for iPhone
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
import Foundation
|
||||
|
||||
public struct RemoteTrack: Identifiable, Codable, Hashable, Sendable {
|
||||
public var id: String { trackId }
|
||||
|
||||
public var trackId: String
|
||||
public var title: String
|
||||
public var artist: String
|
||||
public var durationSeconds: Int
|
||||
public var sha256: String
|
||||
public var assetId: String
|
||||
public var createdAt: String
|
||||
public var updatedAt: String
|
||||
|
||||
public init(
|
||||
trackId: String,
|
||||
title: String,
|
||||
artist: String,
|
||||
durationSeconds: Int,
|
||||
sha256: String,
|
||||
assetId: String,
|
||||
createdAt: String,
|
||||
updatedAt: String
|
||||
) {
|
||||
self.trackId = trackId
|
||||
self.title = title
|
||||
self.artist = artist
|
||||
self.durationSeconds = durationSeconds
|
||||
self.sha256 = sha256
|
||||
self.assetId = assetId
|
||||
self.createdAt = createdAt
|
||||
self.updatedAt = updatedAt
|
||||
}
|
||||
}
|
||||
@@ -21,5 +21,12 @@ let package = Package(
|
||||
name: "VelodyNetworking",
|
||||
dependencies: ["VelodyDomain"]
|
||||
),
|
||||
.testTarget(
|
||||
name: "VelodyNetworkingTests",
|
||||
dependencies: [
|
||||
"VelodyDomain",
|
||||
"VelodyNetworking",
|
||||
]
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import Foundation
|
||||
import VelodyDomain
|
||||
|
||||
public struct RemoteTrackDTO: Codable, Hashable, Sendable {
|
||||
public var trackId: String
|
||||
public var title: String
|
||||
public var artist: String
|
||||
public var durationSeconds: Int
|
||||
public var sha256: String
|
||||
public var assetId: String
|
||||
public var createdAt: String
|
||||
public var updatedAt: String
|
||||
|
||||
public init(
|
||||
trackId: String,
|
||||
title: String,
|
||||
artist: String,
|
||||
durationSeconds: Int,
|
||||
sha256: String,
|
||||
assetId: String,
|
||||
createdAt: String,
|
||||
updatedAt: String
|
||||
) {
|
||||
self.trackId = trackId
|
||||
self.title = title
|
||||
self.artist = artist
|
||||
self.durationSeconds = durationSeconds
|
||||
self.sha256 = sha256
|
||||
self.assetId = assetId
|
||||
self.createdAt = createdAt
|
||||
self.updatedAt = updatedAt
|
||||
}
|
||||
|
||||
public var remoteTrack: RemoteTrack {
|
||||
RemoteTrack(
|
||||
trackId: trackId,
|
||||
title: title,
|
||||
artist: artist,
|
||||
durationSeconds: durationSeconds,
|
||||
sha256: sha256,
|
||||
assetId: assetId,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public struct RemoteLibraryResponseDTO: Codable, Hashable, Sendable {
|
||||
public var tracks: [RemoteTrackDTO]
|
||||
|
||||
public init(tracks: [RemoteTrackDTO]) {
|
||||
self.tracks = tracks
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,10 @@ public protocol VelodyAPIClient: Sendable {
|
||||
|
||||
func fetchSyncBootstrap() async throws -> SyncBootstrapResponse
|
||||
|
||||
func fetchRemoteLibrary(
|
||||
deviceId: String
|
||||
) async throws -> RemoteLibraryResponseDTO
|
||||
|
||||
func prepareUpload(
|
||||
_ payload: UploadPrepareRequest
|
||||
) async throws -> UploadPrepareResponse
|
||||
@@ -106,6 +110,19 @@ public struct URLSessionVelodyAPIClient: VelodyAPIClient {
|
||||
)
|
||||
}
|
||||
|
||||
public func fetchRemoteLibrary(
|
||||
deviceId: String
|
||||
) async throws -> RemoteLibraryResponseDTO {
|
||||
try await sendRequest(
|
||||
method: "GET",
|
||||
pathComponents: ["api", "v1", "library", "tracks"],
|
||||
queryItems: [
|
||||
URLQueryItem(name: "deviceId", value: deviceId),
|
||||
],
|
||||
responseType: RemoteLibraryResponseDTO.self
|
||||
)
|
||||
}
|
||||
|
||||
public func prepareUpload(
|
||||
_ payload: UploadPrepareRequest
|
||||
) async throws -> UploadPrepareResponse {
|
||||
@@ -139,6 +156,7 @@ public struct URLSessionVelodyAPIClient: VelodyAPIClient {
|
||||
let request = try buildRequest(
|
||||
method: "PUT",
|
||||
pathComponents: ["api", "v1", "uploads", uploadId, "file"],
|
||||
queryItems: [],
|
||||
bodyData: nil,
|
||||
contentType: mimeType
|
||||
)
|
||||
@@ -174,11 +192,13 @@ public struct URLSessionVelodyAPIClient: VelodyAPIClient {
|
||||
private func sendRequest<Response: Decodable>(
|
||||
method: String,
|
||||
pathComponents: [String],
|
||||
queryItems: [URLQueryItem] = [],
|
||||
responseType: Response.Type
|
||||
) async throws -> Response {
|
||||
let request = try buildRequest(
|
||||
method: method,
|
||||
pathComponents: pathComponents,
|
||||
queryItems: queryItems,
|
||||
bodyData: nil
|
||||
)
|
||||
|
||||
@@ -188,6 +208,7 @@ public struct URLSessionVelodyAPIClient: VelodyAPIClient {
|
||||
private func sendRequest<Body: Encodable, Response: Decodable>(
|
||||
method: String,
|
||||
pathComponents: [String],
|
||||
queryItems: [URLQueryItem] = [],
|
||||
body: Body,
|
||||
responseType: Response.Type
|
||||
) async throws -> Response {
|
||||
@@ -202,6 +223,7 @@ public struct URLSessionVelodyAPIClient: VelodyAPIClient {
|
||||
let request = try buildRequest(
|
||||
method: method,
|
||||
pathComponents: pathComponents,
|
||||
queryItems: queryItems,
|
||||
bodyData: bodyData,
|
||||
contentType: "application/json"
|
||||
)
|
||||
@@ -212,10 +234,14 @@ public struct URLSessionVelodyAPIClient: VelodyAPIClient {
|
||||
private func buildRequest(
|
||||
method: String,
|
||||
pathComponents: [String],
|
||||
queryItems: [URLQueryItem],
|
||||
bodyData: Data?,
|
||||
contentType: String? = nil
|
||||
) throws -> URLRequest {
|
||||
guard let url = endpointURL(pathComponents: pathComponents) else {
|
||||
guard let url = endpointURL(
|
||||
pathComponents: pathComponents,
|
||||
queryItems: queryItems
|
||||
) else {
|
||||
throw VelodyAPIError.invalidServerURL(environment.baseURL.absoluteString)
|
||||
}
|
||||
|
||||
@@ -284,10 +310,24 @@ public struct URLSessionVelodyAPIClient: VelodyAPIClient {
|
||||
}
|
||||
}
|
||||
|
||||
private func endpointURL(pathComponents: [String]) -> URL? {
|
||||
pathComponents.reduce(environment.baseURL) { partialURL, component in
|
||||
private func endpointURL(
|
||||
pathComponents: [String],
|
||||
queryItems: [URLQueryItem]
|
||||
) -> URL? {
|
||||
let baseURL = pathComponents.reduce(environment.baseURL) { partialURL, component in
|
||||
partialURL.appendingPathComponent(component, isDirectory: false)
|
||||
}
|
||||
|
||||
guard !queryItems.isEmpty else {
|
||||
return baseURL
|
||||
}
|
||||
|
||||
guard var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
components.queryItems = queryItems
|
||||
return components.url
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,6 +380,27 @@ public struct StubVelodyAPIClient: VelodyAPIClient {
|
||||
)
|
||||
}
|
||||
|
||||
public func fetchRemoteLibrary(
|
||||
deviceId: String
|
||||
) async throws -> RemoteLibraryResponseDTO {
|
||||
_ = deviceId
|
||||
|
||||
return RemoteLibraryResponseDTO(
|
||||
tracks: [
|
||||
RemoteTrackDTO(
|
||||
trackId: UUID().uuidString,
|
||||
title: "Velody Remote Placeholder",
|
||||
artist: "Private Library",
|
||||
durationSeconds: 245,
|
||||
sha256: String(repeating: "a", count: 64),
|
||||
assetId: UUID().uuidString,
|
||||
createdAt: ISO8601DateFormatter().string(from: .now),
|
||||
updatedAt: ISO8601DateFormatter().string(from: .now)
|
||||
),
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
public func prepareUpload(
|
||||
_ payload: UploadPrepareRequest
|
||||
) async throws -> UploadPrepareResponse {
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import Foundation
|
||||
import XCTest
|
||||
@testable import VelodyNetworking
|
||||
|
||||
final class RemoteLibraryDTOTests: XCTestCase {
|
||||
func testRemoteTrackDTOMapsToRemoteTrack() {
|
||||
let dto = RemoteTrackDTO(
|
||||
trackId: "track-123",
|
||||
title: "Remote Title",
|
||||
artist: "Remote Artist",
|
||||
durationSeconds: 245,
|
||||
sha256: String(repeating: "a", count: 64),
|
||||
assetId: "asset-456",
|
||||
createdAt: "2026-05-29T08:00:00.000Z",
|
||||
updatedAt: "2026-05-29T08:05:00.000Z"
|
||||
)
|
||||
|
||||
let track = dto.remoteTrack
|
||||
|
||||
XCTAssertEqual(track.trackId, "track-123")
|
||||
XCTAssertEqual(track.title, "Remote Title")
|
||||
XCTAssertEqual(track.artist, "Remote Artist")
|
||||
XCTAssertEqual(track.durationSeconds, 245)
|
||||
XCTAssertEqual(track.sha256, String(repeating: "a", count: 64))
|
||||
XCTAssertEqual(track.assetId, "asset-456")
|
||||
}
|
||||
|
||||
func testRemoteLibraryResponseDTODecodesFromAPIResponse() throws {
|
||||
let data = Data(
|
||||
"""
|
||||
{
|
||||
"tracks": [
|
||||
{
|
||||
"trackId": "track-123",
|
||||
"title": "Remote Title",
|
||||
"artist": "Remote Artist",
|
||||
"durationSeconds": 245,
|
||||
"sha256": "\(String(repeating: "a", count: 64))",
|
||||
"assetId": "asset-456",
|
||||
"createdAt": "2026-05-29T08:00:00.000Z",
|
||||
"updatedAt": "2026-05-29T08:05:00.000Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
""".utf8
|
||||
)
|
||||
|
||||
let decoded = try JSONDecoder().decode(RemoteLibraryResponseDTO.self, from: data)
|
||||
|
||||
XCTAssertEqual(decoded.tracks.count, 1)
|
||||
XCTAssertEqual(decoded.tracks.first?.trackId, "track-123")
|
||||
XCTAssertEqual(decoded.tracks.first?.durationSeconds, 245)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import Foundation
|
||||
import VelodyDomain
|
||||
|
||||
public protocol RemoteLibraryStore: Actor {
|
||||
func loadRemoteTracks() async throws -> [RemoteTrack]
|
||||
func replaceRemoteTracks(_ tracks: [RemoteTrack]) async throws
|
||||
}
|
||||
|
||||
public actor FileRemoteLibraryStore: RemoteLibraryStore {
|
||||
private let fileURL: URL
|
||||
private let fileManager: FileManager
|
||||
private let encoder = JSONEncoder()
|
||||
private let decoder = JSONDecoder()
|
||||
|
||||
public init(
|
||||
fileURL: URL? = nil,
|
||||
fileManager: FileManager = .default
|
||||
) throws {
|
||||
self.fileManager = fileManager
|
||||
if let fileURL {
|
||||
self.fileURL = fileURL
|
||||
} else {
|
||||
self.fileURL = try Self.defaultFileURL(fileManager: fileManager)
|
||||
}
|
||||
}
|
||||
|
||||
public func loadRemoteTracks() async throws -> [RemoteTrack] {
|
||||
guard fileManager.fileExists(atPath: fileURL.path) else {
|
||||
return []
|
||||
}
|
||||
|
||||
let data = try Data(contentsOf: fileURL)
|
||||
return try decoder.decode([RemoteTrack].self, from: data)
|
||||
}
|
||||
|
||||
public func replaceRemoteTracks(_ tracks: [RemoteTrack]) async throws {
|
||||
try fileManager.createDirectory(
|
||||
at: fileURL.deletingLastPathComponent(),
|
||||
withIntermediateDirectories: true
|
||||
)
|
||||
|
||||
let data = try encoder.encode(tracks)
|
||||
try data.write(to: fileURL, options: .atomic)
|
||||
}
|
||||
|
||||
private static func defaultFileURL(fileManager: FileManager) throws -> URL {
|
||||
guard let applicationSupportURL = fileManager.urls(
|
||||
for: .applicationSupportDirectory,
|
||||
in: .userDomainMask
|
||||
).first else {
|
||||
throw CocoaError(.fileNoSuchFile)
|
||||
}
|
||||
|
||||
return applicationSupportURL
|
||||
.appendingPathComponent("Velody", isDirectory: true)
|
||||
.appendingPathComponent("remote-library.json")
|
||||
}
|
||||
}
|
||||
|
||||
public actor InMemoryRemoteLibraryStore: RemoteLibraryStore {
|
||||
private var tracks: [RemoteTrack]
|
||||
|
||||
public init(tracks: [RemoteTrack] = []) {
|
||||
self.tracks = tracks
|
||||
}
|
||||
|
||||
public func loadRemoteTracks() async throws -> [RemoteTrack] {
|
||||
tracks
|
||||
}
|
||||
|
||||
public func replaceRemoteTracks(_ tracks: [RemoteTrack]) async throws {
|
||||
self.tracks = tracks
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import Foundation
|
||||
import XCTest
|
||||
import VelodyDomain
|
||||
@testable import VelodyPersistence
|
||||
|
||||
final class RemoteLibraryStoreTests: XCTestCase {
|
||||
func testFileRemoteLibraryStorePersistsTracksAcrossInstances() async throws {
|
||||
let fileManager = FileManager.default
|
||||
let tempDirectory = fileManager.temporaryDirectory.appendingPathComponent(
|
||||
UUID().uuidString,
|
||||
isDirectory: true
|
||||
)
|
||||
let fileURL = tempDirectory.appendingPathComponent("remote-library.json")
|
||||
|
||||
defer {
|
||||
try? fileManager.removeItem(at: tempDirectory)
|
||||
}
|
||||
|
||||
let firstStore = try FileRemoteLibraryStore(fileURL: fileURL)
|
||||
let tracks = [
|
||||
RemoteTrack(
|
||||
trackId: "track-123",
|
||||
title: "Remote Title",
|
||||
artist: "Remote Artist",
|
||||
durationSeconds: 245,
|
||||
sha256: String(repeating: "a", count: 64),
|
||||
assetId: "asset-456",
|
||||
createdAt: "2026-05-29T08:00:00.000Z",
|
||||
updatedAt: "2026-05-29T08:05:00.000Z"
|
||||
),
|
||||
]
|
||||
|
||||
try await firstStore.replaceRemoteTracks(tracks)
|
||||
|
||||
let secondStore = try FileRemoteLibraryStore(fileURL: fileURL)
|
||||
let restoredTracks = try await secondStore.loadRemoteTracks()
|
||||
|
||||
XCTAssertEqual(restoredTracks, tracks)
|
||||
}
|
||||
}
|
||||
@@ -27,5 +27,14 @@ let package = Package(
|
||||
"VelodyPersistence",
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "VelodySyncTests",
|
||||
dependencies: [
|
||||
"VelodyDomain",
|
||||
"VelodyNetworking",
|
||||
"VelodyPersistence",
|
||||
"VelodySync",
|
||||
]
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import Foundation
|
||||
import VelodyDomain
|
||||
import VelodyNetworking
|
||||
import VelodyPersistence
|
||||
|
||||
public protocol RemoteLibraryRepository: Actor {
|
||||
func loadCachedRemoteTracks() async throws -> [RemoteTrack]
|
||||
func syncRemoteTracks(deviceId: String) async throws -> [RemoteTrack]
|
||||
}
|
||||
|
||||
public actor DefaultRemoteLibraryRepository: RemoteLibraryRepository {
|
||||
private let apiClient: any VelodyAPIClient
|
||||
private let store: any RemoteLibraryStore
|
||||
|
||||
public init(
|
||||
apiClient: any VelodyAPIClient,
|
||||
store: any RemoteLibraryStore
|
||||
) {
|
||||
self.apiClient = apiClient
|
||||
self.store = store
|
||||
}
|
||||
|
||||
public func loadCachedRemoteTracks() async throws -> [RemoteTrack] {
|
||||
try await store.loadRemoteTracks()
|
||||
}
|
||||
|
||||
public func syncRemoteTracks(deviceId: String) async throws -> [RemoteTrack] {
|
||||
let response = try await apiClient.fetchRemoteLibrary(deviceId: deviceId)
|
||||
let tracks = response.tracks.map(\.remoteTrack)
|
||||
try await store.replaceRemoteTracks(tracks)
|
||||
return tracks
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import Foundation
|
||||
import VelodyDomain
|
||||
|
||||
public actor RemoteLibrarySyncService {
|
||||
private let repository: any RemoteLibraryRepository
|
||||
|
||||
public init(repository: any RemoteLibraryRepository) {
|
||||
self.repository = repository
|
||||
}
|
||||
|
||||
public func loadCachedRemoteTracks() async throws -> [RemoteTrack] {
|
||||
try await repository.loadCachedRemoteTracks()
|
||||
}
|
||||
|
||||
public func syncRemoteLibrary(deviceId: String) async throws -> [RemoteTrack] {
|
||||
try await repository.syncRemoteTracks(deviceId: deviceId)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
import Foundation
|
||||
import XCTest
|
||||
import VelodyDomain
|
||||
import VelodyNetworking
|
||||
import VelodyPersistence
|
||||
@testable import VelodySync
|
||||
|
||||
final class RemoteLibrarySyncServiceTests: XCTestCase {
|
||||
func testSuccessfulSyncPersistsRemoteTracks() async throws {
|
||||
let store = InMemoryRemoteLibraryStore()
|
||||
let service = RemoteLibrarySyncService(
|
||||
repository: DefaultRemoteLibraryRepository(
|
||||
apiClient: MockVelodyAPIClient(
|
||||
remoteLibraryResponse: RemoteLibraryResponseDTO(
|
||||
tracks: [
|
||||
RemoteTrackDTO(
|
||||
trackId: "track-123",
|
||||
title: "Remote Title",
|
||||
artist: "Remote Artist",
|
||||
durationSeconds: 245,
|
||||
sha256: String(repeating: "a", count: 64),
|
||||
assetId: "asset-456",
|
||||
createdAt: "2026-05-29T08:00:00.000Z",
|
||||
updatedAt: "2026-05-29T08:05:00.000Z"
|
||||
),
|
||||
]
|
||||
)
|
||||
),
|
||||
store: store
|
||||
)
|
||||
)
|
||||
|
||||
let tracks = try await service.syncRemoteLibrary(deviceId: "device-123")
|
||||
let cachedTracks = try await service.loadCachedRemoteTracks()
|
||||
|
||||
XCTAssertEqual(tracks.count, 1)
|
||||
XCTAssertEqual(cachedTracks, tracks)
|
||||
XCTAssertEqual(cachedTracks.first?.trackId, "track-123")
|
||||
}
|
||||
|
||||
func testEmptyResponseClearsCachedRemoteLibrary() async throws {
|
||||
let store = InMemoryRemoteLibraryStore(
|
||||
tracks: [
|
||||
RemoteTrack(
|
||||
trackId: "track-123",
|
||||
title: "Old",
|
||||
artist: "Artist",
|
||||
durationSeconds: 100,
|
||||
sha256: String(repeating: "b", count: 64),
|
||||
assetId: "asset-123",
|
||||
createdAt: "2026-05-29T08:00:00.000Z",
|
||||
updatedAt: "2026-05-29T08:05:00.000Z"
|
||||
),
|
||||
]
|
||||
)
|
||||
let service = RemoteLibrarySyncService(
|
||||
repository: DefaultRemoteLibraryRepository(
|
||||
apiClient: MockVelodyAPIClient(
|
||||
remoteLibraryResponse: RemoteLibraryResponseDTO(tracks: [])
|
||||
),
|
||||
store: store
|
||||
)
|
||||
)
|
||||
|
||||
let tracks = try await service.syncRemoteLibrary(deviceId: "device-123")
|
||||
let cachedTracks = try await service.loadCachedRemoteTracks()
|
||||
|
||||
XCTAssertEqual(tracks, [])
|
||||
XCTAssertEqual(cachedTracks, [])
|
||||
}
|
||||
|
||||
func testNetworkFailureLeavesCachedRemoteLibraryIntact() async throws {
|
||||
let cachedTrack = RemoteTrack(
|
||||
trackId: "track-123",
|
||||
title: "Cached",
|
||||
artist: "Artist",
|
||||
durationSeconds: 100,
|
||||
sha256: String(repeating: "c", count: 64),
|
||||
assetId: "asset-123",
|
||||
createdAt: "2026-05-29T08:00:00.000Z",
|
||||
updatedAt: "2026-05-29T08:05:00.000Z"
|
||||
)
|
||||
let store = InMemoryRemoteLibraryStore(tracks: [cachedTrack])
|
||||
let service = RemoteLibrarySyncService(
|
||||
repository: DefaultRemoteLibraryRepository(
|
||||
apiClient: MockVelodyAPIClient(
|
||||
remoteLibraryError: VelodyAPIError.requestFailed("Offline")
|
||||
),
|
||||
store: store
|
||||
)
|
||||
)
|
||||
|
||||
await XCTAssertThrowsErrorAsync {
|
||||
_ = try await service.syncRemoteLibrary(deviceId: "device-123")
|
||||
}
|
||||
|
||||
let cachedTracks = try await service.loadCachedRemoteTracks()
|
||||
XCTAssertEqual(cachedTracks, [cachedTrack])
|
||||
}
|
||||
}
|
||||
|
||||
private struct MockVelodyAPIClient: VelodyAPIClient {
|
||||
let remoteLibraryResponse: RemoteLibraryResponseDTO?
|
||||
let remoteLibraryError: VelodyAPIError?
|
||||
|
||||
init(
|
||||
remoteLibraryResponse: RemoteLibraryResponseDTO? = nil,
|
||||
remoteLibraryError: VelodyAPIError? = nil
|
||||
) {
|
||||
self.remoteLibraryResponse = remoteLibraryResponse
|
||||
self.remoteLibraryError = remoteLibraryError
|
||||
}
|
||||
|
||||
func registerDevice(
|
||||
_ payload: DeviceRegistrationPayload
|
||||
) async throws -> DeviceRegistrationResponse {
|
||||
_ = payload
|
||||
return DeviceRegistrationResponse(
|
||||
deviceId: UUID().uuidString,
|
||||
bootstrapToken: UUID().uuidString,
|
||||
serverTime: "2026-05-29T08:00:00.000Z"
|
||||
)
|
||||
}
|
||||
|
||||
func sendHeartbeat(
|
||||
_ payload: DeviceHeartbeatPayload
|
||||
) async throws -> DeviceHeartbeatResponse {
|
||||
_ = payload
|
||||
return DeviceHeartbeatResponse(
|
||||
ok: true,
|
||||
serverTime: "2026-05-29T08:00:00.000Z"
|
||||
)
|
||||
}
|
||||
|
||||
func fetchSyncBootstrap() async throws -> SyncBootstrapResponse {
|
||||
SyncBootstrapResponse(
|
||||
nextCursor: SyncCursor(value: "0"),
|
||||
tracks: [],
|
||||
events: [],
|
||||
deletedTrackIds: [],
|
||||
serverTime: "2026-05-29T08:00:00.000Z"
|
||||
)
|
||||
}
|
||||
|
||||
func fetchRemoteLibrary(
|
||||
deviceId: String
|
||||
) async throws -> RemoteLibraryResponseDTO {
|
||||
_ = deviceId
|
||||
|
||||
if let remoteLibraryError {
|
||||
throw remoteLibraryError
|
||||
}
|
||||
|
||||
return remoteLibraryResponse ?? RemoteLibraryResponseDTO(tracks: [])
|
||||
}
|
||||
|
||||
func prepareUpload(
|
||||
_ payload: UploadPrepareRequest
|
||||
) async throws -> UploadPrepareResponse {
|
||||
_ = payload
|
||||
return UploadPrepareResponse(status: .uploadRequired, uploadId: UUID().uuidString, nextOffset: 0)
|
||||
}
|
||||
|
||||
func fetchUploadStatus(
|
||||
uploadId: String
|
||||
) async throws -> UploadSessionStatusResponse {
|
||||
UploadSessionStatusResponse(
|
||||
uploadId: uploadId,
|
||||
status: .completed,
|
||||
receivedBytes: "0",
|
||||
expectedSizeBytes: "0",
|
||||
nextOffset: "0"
|
||||
)
|
||||
}
|
||||
|
||||
func uploadFile(
|
||||
uploadId: String,
|
||||
fileURL: URL,
|
||||
mimeType: String
|
||||
) async throws -> UploadSessionStatusResponse {
|
||||
_ = fileURL
|
||||
_ = mimeType
|
||||
return UploadSessionStatusResponse(
|
||||
uploadId: uploadId,
|
||||
status: .completed,
|
||||
receivedBytes: "0",
|
||||
expectedSizeBytes: "0",
|
||||
nextOffset: "0"
|
||||
)
|
||||
}
|
||||
|
||||
func finalizeUpload(
|
||||
uploadId: String,
|
||||
payload: UploadFinalizeRequest
|
||||
) async throws -> UploadFinalizeResponse {
|
||||
_ = uploadId
|
||||
_ = payload
|
||||
return UploadFinalizeResponse(
|
||||
trackId: UUID().uuidString,
|
||||
assetId: UUID().uuidString
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func XCTAssertThrowsErrorAsync(
|
||||
_ expression: @escaping () async throws -> Void,
|
||||
file: StaticString = #filePath,
|
||||
line: UInt = #line
|
||||
) async {
|
||||
do {
|
||||
try await expression()
|
||||
XCTFail("Expected expression to throw an error.", file: file, line: line)
|
||||
} catch {
|
||||
XCTAssertTrue(true)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user